mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
chat 2.0
This commit is contained in:
parent
15a18550d9
commit
e840db1991
21 changed files with 518 additions and 252 deletions
|
@ -83,7 +83,7 @@ def nodeinfo():
|
||||||
@bp.route('/.well-known/host-meta')
|
@bp.route('/.well-known/host-meta')
|
||||||
@cache.cached(timeout=600)
|
@cache.cached(timeout=600)
|
||||||
def host_meta():
|
def host_meta():
|
||||||
resp = make_response(f'<?xml version="1.0" encoding="UTF-8"?>\n<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n<Link rel="lrdd" template="https://{current_app.config["SERVER_NAME"]}/.well-known/webfinger?resource={uri}"/>\n</XRD>')
|
resp = make_response('<?xml version="1.0" encoding="UTF-8"?>\n<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n<Link rel="lrdd" template="https://' + current_app.config["SERVER_NAME"] + '/.well-known/webfinger?resource={uri}"/>\n</XRD>')
|
||||||
resp.content_type = 'application/xrd+xml; charset=utf-8'
|
resp.content_type = 'application/xrd+xml; charset=utf-8'
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -380,7 +380,9 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
||||||
recipient_ap_id = request_json['object']['to'][0]
|
recipient_ap_id = request_json['object']['to'][0]
|
||||||
recipient = find_actor_or_create(recipient_ap_id)
|
recipient = find_actor_or_create(recipient_ap_id)
|
||||||
if sender and recipient and recipient.is_local():
|
if sender and recipient and recipient.is_local():
|
||||||
if recipient.has_blocked_user(sender.id) or recipient.has_blocked_instance(sender.instance_id):
|
if sender.created_recently() or sender.reputation < 10:
|
||||||
|
activity_log.exception_message = "Sender not eligible to send"
|
||||||
|
elif recipient.has_blocked_user(sender.id) or recipient.has_blocked_instance(sender.instance_id):
|
||||||
activity_log.exception_message = "Sender blocked by recipient"
|
activity_log.exception_message = "Sender blocked by recipient"
|
||||||
else:
|
else:
|
||||||
# Save ChatMessage to DB
|
# Save ChatMessage to DB
|
||||||
|
|
|
@ -98,10 +98,13 @@ class EditTopicForm(FlaskForm):
|
||||||
|
|
||||||
class EditUserForm(FlaskForm):
|
class EditUserForm(FlaskForm):
|
||||||
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
|
email = StringField(_l('Email address'), validators=[Optional(), Length(max=255)])
|
||||||
matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)])
|
matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)])
|
||||||
profile_file = FileField(_l('Avatar image'))
|
profile_file = FileField(_l('Avatar image'))
|
||||||
banner_file = FileField(_l('Top banner image'))
|
banner_file = FileField(_l('Top banner image'))
|
||||||
bot = BooleanField(_l('This profile is a bot'))
|
bot = BooleanField(_l('This profile is a bot'))
|
||||||
|
verified = BooleanField(_l('Email address is verified'))
|
||||||
|
banned = BooleanField(_l('Banned'))
|
||||||
newsletter = BooleanField(_l('Subscribe to email newsletter'))
|
newsletter = BooleanField(_l('Subscribe to email newsletter'))
|
||||||
ignore_bots = BooleanField(_l('Hide posts by bots'))
|
ignore_bots = BooleanField(_l('Hide posts by bots'))
|
||||||
nsfw = BooleanField(_l('Show NSFW posts'))
|
nsfw = BooleanField(_l('Show NSFW posts'))
|
||||||
|
|
|
@ -484,9 +484,12 @@ def admin_user_edit(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user.about = form.about.data
|
user.about = form.about.data
|
||||||
|
user.email = form.email.data
|
||||||
user.about_html = markdown_to_html(form.about.data)
|
user.about_html = markdown_to_html(form.about.data)
|
||||||
user.matrix_user_id = form.matrix_user_id.data
|
user.matrix_user_id = form.matrix_user_id.data
|
||||||
user.bot = form.bot.data
|
user.bot = form.bot.data
|
||||||
|
user.verified = form.verified.data
|
||||||
|
user.banned = form.banned.data
|
||||||
profile_file = request.files['profile_file']
|
profile_file = request.files['profile_file']
|
||||||
if profile_file and profile_file.filename != '':
|
if profile_file and profile_file.filename != '':
|
||||||
# remove old avatar
|
# remove old avatar
|
||||||
|
@ -528,9 +531,12 @@ def admin_user_edit(user_id):
|
||||||
if not user.is_local():
|
if not user.is_local():
|
||||||
flash(_('This is a remote user - most settings here will be regularly overwritten with data from the original server.'), 'warning')
|
flash(_('This is a remote user - most settings here will be regularly overwritten with data from the original server.'), 'warning')
|
||||||
form.about.data = user.about
|
form.about.data = user.about
|
||||||
|
form.email.data = user.email
|
||||||
form.matrix_user_id.data = user.matrix_user_id
|
form.matrix_user_id.data = user.matrix_user_id
|
||||||
form.newsletter.data = user.newsletter
|
form.newsletter.data = user.newsletter
|
||||||
form.bot.data = user.bot
|
form.bot.data = user.bot
|
||||||
|
form.verified.data = user.verified
|
||||||
|
form.banned.data = user.banned
|
||||||
form.ignore_bots.data = user.ignore_bots
|
form.ignore_bots.data = user.ignore_bots
|
||||||
form.nsfw.data = user.show_nsfw
|
form.nsfw.data = user.show_nsfw
|
||||||
form.nsfl.data = user.show_nsfl
|
form.nsfl.data = user.show_nsfl
|
||||||
|
|
|
@ -38,7 +38,7 @@ def login():
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
flash(_('Invalid password'))
|
flash(_('Invalid password'))
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
if user.banned or user_ip_banned() or user_cookie_banned():
|
if user.id != 1 and (user.banned or user_ip_banned() or user_cookie_banned()):
|
||||||
flash(_('You have been banned.'), 'error')
|
flash(_('You have been banned.'), 'error')
|
||||||
|
|
||||||
response = make_response(redirect(url_for('auth.login')))
|
response = make_response(redirect(url_for('auth.login')))
|
||||||
|
|
|
@ -6,9 +6,36 @@ from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Le
|
||||||
from flask_babel import _, lazy_gettext as _l
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.utils import MultiCheckboxField
|
||||||
|
|
||||||
|
|
||||||
class AddReply(FlaskForm):
|
class AddReply(FlaskForm):
|
||||||
message = TextAreaField(_l('Message'), validators=[DataRequired(), Length(min=1, max=5000)], render_kw={'placeholder': 'Type a reply here...'})
|
message = TextAreaField(_l('Message'), validators=[DataRequired(), Length(min=1, max=5000)], render_kw={'placeholder': 'Type a reply here...'})
|
||||||
recipient_id = HiddenField()
|
|
||||||
submit = SubmitField(_l('Reply'))
|
submit = SubmitField(_l('Reply'))
|
||||||
|
|
||||||
|
|
||||||
|
class ReportConversationForm(FlaskForm):
|
||||||
|
reason_choices = [('7', _l('Spam')),
|
||||||
|
('2', _l('Harassment')),
|
||||||
|
('3', _l('Threatening violence')),
|
||||||
|
('4', _l('Promoting hate / genocide')),
|
||||||
|
('15', _l('Misinformation / disinformation')),
|
||||||
|
('16', _l('Racism, sexism, transphobia')),
|
||||||
|
('5', _l('Minor abuse or sexualization')),
|
||||||
|
('8', _l('Non-consensual intimate media')),
|
||||||
|
('9', _l('Prohibited transaction')), ('10', _l('Impersonation')),
|
||||||
|
('11', _l('Copyright violation')), ('12', _l('Trademark violation')),
|
||||||
|
('13', _l('Self-harm or suicide')),
|
||||||
|
('14', _l('Other'))]
|
||||||
|
reasons = MultiCheckboxField(_l('Reason'), choices=reason_choices)
|
||||||
|
description = StringField(_l('More info'))
|
||||||
|
report_remote = BooleanField('Also send report to originating instance')
|
||||||
|
submit = SubmitField(_l('Report'))
|
||||||
|
|
||||||
|
def reasons_to_string(self, reason_data) -> str:
|
||||||
|
result = []
|
||||||
|
for reason_id in reason_data:
|
||||||
|
for choice in self.reason_choices:
|
||||||
|
if choice[0] == reason_id:
|
||||||
|
result.append(str(choice[1]))
|
||||||
|
return ', '.join(result)
|
||||||
|
|
|
@ -1,84 +1,37 @@
|
||||||
from datetime import datetime, timedelta
|
from flask import request, flash, json, url_for, current_app, redirect, g, abort
|
||||||
|
|
||||||
from flask import request, flash, json, url_for, current_app, redirect, g
|
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import desc, or_, and_, text
|
from sqlalchemy import desc, or_, and_, text
|
||||||
|
|
||||||
from app import db, celery
|
from app import db, celery
|
||||||
from app.activitypub.signature import post_request
|
from app.chat.forms import AddReply, ReportConversationForm
|
||||||
from app.chat.forms import AddReply
|
from app.chat.util import send_message, find_existing_conversation
|
||||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
from app.models import Site, User, Report, ChatMessage, Notification, InstanceBlock, Conversation, conversation_member
|
||||||
User, Instance, File, Report, Topic, UserRegistration, ChatMessage, Notification, InstanceBlock
|
|
||||||
from app.user.forms import ReportUserForm
|
from app.user.forms import ReportUserForm
|
||||||
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
from app.utils import render_template, moderating_communities, joined_communities
|
||||||
moderating_communities, joined_communities, finalize_user_setup, theme_list, allowlist_html, shorten_string
|
|
||||||
from app.chat import bp
|
from app.chat import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/chat', methods=['GET', 'POST'])
|
@bp.route('/chat', methods=['GET', 'POST'])
|
||||||
@bp.route('/chat/<int:sender_id>', methods=['GET', 'POST'])
|
@bp.route('/chat/<int:conversation_id>', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def chat_home(sender_id=None):
|
def chat_home(conversation_id=None):
|
||||||
form = AddReply()
|
form = AddReply()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
recipient = User.query.get(form.recipient_id.data)
|
reply = send_message(form, conversation_id)
|
||||||
reply = ChatMessage(sender_id=current_user.id, recipient_id=recipient.id,
|
return redirect(url_for('chat.chat_home', conversation_id=conversation_id, _anchor=f'message_{reply.id}'))
|
||||||
body=form.message.data, body_html=allowlist_html(markdown_to_html(form.message.data)))
|
|
||||||
if recipient.is_local():
|
|
||||||
# Notify local recipient
|
|
||||||
notify = Notification(title=shorten_string('New message from ' + current_user.display_name()), url='/chat/' + str(current_user.id),
|
|
||||||
user_id=recipient.id,
|
|
||||||
author_id=current_user.id)
|
|
||||||
db.session.add(notify)
|
|
||||||
recipient.unread_notifications += 1
|
|
||||||
db.session.add(reply)
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
else:
|
||||||
db.session.add(reply)
|
conversations = Conversation.query.join(conversation_member,
|
||||||
db.session.commit()
|
conversation_member.c.conversation_id == Conversation.id). \
|
||||||
# Federate reply
|
filter(conversation_member.c.user_id == current_user.id).order_by(desc(Conversation.updated_at)).limit(50).all()
|
||||||
reply_json = {
|
if conversation_id is None:
|
||||||
"actor": current_user.profile_id(),
|
return redirect(url_for('chat.chat_home', conversation_id=conversations[0].id))
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
|
||||||
"object": {
|
|
||||||
"attributedTo": current_user.profile_id(),
|
|
||||||
"content": reply.body_html,
|
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/private_message/{reply.id}",
|
|
||||||
"mediaType": "text/html",
|
|
||||||
"published": utcnow().isoformat() + 'Z', # Lemmy is inconsistent with the date format they use
|
|
||||||
"source": {
|
|
||||||
"content": reply.body,
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"to": [
|
|
||||||
recipient.profile_id()
|
|
||||||
],
|
|
||||||
"type": "ChatMessage"
|
|
||||||
},
|
|
||||||
"to": [
|
|
||||||
recipient.profile_id()
|
|
||||||
],
|
|
||||||
"type": "Create"
|
|
||||||
}
|
|
||||||
success = post_request(recipient.ap_inbox_url, reply_json, current_user.private_key,
|
|
||||||
current_user.profile_id() + '#main-key')
|
|
||||||
if not success:
|
|
||||||
flash(_('Message failed to send to remote server. Try again later.'), 'error')
|
|
||||||
|
|
||||||
return redirect(url_for('chat.chat_home', sender_id=recipient.id, _anchor=f'message_{reply.id}'))
|
|
||||||
else:
|
else:
|
||||||
senders = User.query.filter(User.banned == False).join(ChatMessage, ChatMessage.sender_id == User.id)
|
conversation = Conversation.query.get_or_404(conversation_id)
|
||||||
senders = senders.filter(ChatMessage.recipient_id == current_user.id).order_by(desc(ChatMessage.created_at)).limit(500).all()
|
if not current_user.is_admin() and not conversation.is_member(current_user):
|
||||||
|
abort(400)
|
||||||
if senders:
|
if conversations:
|
||||||
messages_with = senders[0].id if sender_id is None else sender_id
|
messages = conversation.messages.order_by(ChatMessage.created_at).all()
|
||||||
sender_id = messages_with
|
|
||||||
messages = ChatMessage.query.filter(or_(
|
|
||||||
and_(ChatMessage.recipient_id == current_user.id, ChatMessage.sender_id == messages_with),
|
|
||||||
and_(ChatMessage.recipient_id == messages_with, ChatMessage.sender_id == current_user.id))
|
|
||||||
)
|
|
||||||
messages = messages.order_by(ChatMessage.created_at).all()
|
|
||||||
if messages:
|
if messages:
|
||||||
if messages[0].sender_id == current_user.id:
|
if messages[0].sender_id == current_user.id:
|
||||||
other_party = User.query.get(messages[0].recipient_id)
|
other_party = User.query.get(messages[0].recipient_id)
|
||||||
|
@ -86,19 +39,19 @@ def chat_home(sender_id=None):
|
||||||
other_party = User.query.get(messages[0].sender_id)
|
other_party = User.query.get(messages[0].sender_id)
|
||||||
else:
|
else:
|
||||||
other_party = None
|
other_party = None
|
||||||
form.recipient_id.data = messages_with
|
|
||||||
else:
|
else:
|
||||||
messages = []
|
messages = []
|
||||||
other_party = None
|
other_party = None
|
||||||
if sender_id and int(sender_id):
|
|
||||||
sql = f"UPDATE notification SET read = true WHERE url = '/chat/{sender_id}' AND user_id = {current_user.id}"
|
sql = f"UPDATE notification SET read = true WHERE url = '/chat/{conversation_id}' AND user_id = {current_user.id}"
|
||||||
db.session.execute(text(sql))
|
db.session.execute(text(sql))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
current_user.unread_notifications = Notification.query.filter_by(user_id=current_user.id, read=False).count()
|
current_user.unread_notifications = Notification.query.filter_by(user_id=current_user.id, read=False).count()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return render_template('chat/home.html', title=_('Chat with %(name)s', name=other_party.display_name()) if other_party else _('Chat'),
|
return render_template('chat/conversation.html', title=_('Chat with %(name)s', name=other_party.display_name()) if other_party else _('Chat'),
|
||||||
senders=senders, messages=messages, other_party=other_party, form=form,
|
conversations=conversations, messages=messages, form=form,
|
||||||
|
current_conversation=conversation_id, conversation=conversation,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
site=g.site)
|
site=g.site)
|
||||||
|
@ -108,63 +61,88 @@ def chat_home(sender_id=None):
|
||||||
@login_required
|
@login_required
|
||||||
def new_message(to):
|
def new_message(to):
|
||||||
recipient = User.query.get_or_404(to)
|
recipient = User.query.get_or_404(to)
|
||||||
existing_conversation = ChatMessage.query.filter(or_(
|
if current_user.created_recently() or current_user.reputation < 10 or current_user.banned or not current_user.verified:
|
||||||
and_(ChatMessage.recipient_id == current_user.id, ChatMessage.sender_id == recipient.id),
|
return redirect(url_for('chat.denied'))
|
||||||
and_(ChatMessage.recipient_id == recipient.id, ChatMessage.sender_id == current_user.id))
|
if recipient.has_blocked_user(current_user.id) or current_user.has_blocked_user(recipient.id):
|
||||||
).first()
|
return redirect(url_for('chat.blocked'))
|
||||||
|
existing_conversation = find_existing_conversation(recipient=recipient, sender=current_user)
|
||||||
if existing_conversation:
|
if existing_conversation:
|
||||||
return redirect(url_for('chat.home', sender_id=recipient.id, _anchor='submit'))
|
return redirect(url_for('chat.chat_home', conversation_id=existing_conversation.id, _anchor='message'))
|
||||||
form = AddReply()
|
form = AddReply()
|
||||||
|
form.submit.label.text = _('Send')
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
flash(_('Message sent'))
|
conversation = Conversation(user_id=current_user.id)
|
||||||
return redirect(url_for('chat.home', sender_id=recipient.id))
|
conversation.members.append(recipient)
|
||||||
|
conversation.members.append(current_user)
|
||||||
|
db.session.add(conversation)
|
||||||
|
db.session.commit()
|
||||||
|
reply = send_message(form, conversation.id)
|
||||||
|
return redirect(url_for('chat.chat_home', conversation_id=conversation.id, _anchor=f'message_{reply.id}'))
|
||||||
|
else:
|
||||||
|
return render_template('chat/new_message.html', form=form, title=_('New message to "%(recipient_name)s"', recipient_name=recipient.link()),
|
||||||
|
recipient=recipient,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
site=g.site)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/chat/<int:sender_id>/options', methods=['GET', 'POST'])
|
@bp.route('/chat/denied', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def chat_options(sender_id):
|
def denied():
|
||||||
sender = User.query.get_or_404(sender_id)
|
return render_template('chat/denied.html')
|
||||||
return render_template('chat/chat_options.html', sender=sender,
|
|
||||||
|
|
||||||
|
@bp.route('/chat/blocked', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def blocked():
|
||||||
|
return render_template('chat/blocked.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/chat/<int:conversation_id>/options', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def chat_options(conversation_id):
|
||||||
|
conversation = Conversation.query.get_or_404(conversation_id)
|
||||||
|
if current_user.is_admin() or current_user.is_member(current_user):
|
||||||
|
return render_template('chat/chat_options.html', conversation=conversation,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
site=g.site
|
site=g.site
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/chat/<int:sender_id>/delete', methods=['GET', 'POST'])
|
@bp.route('/chat/<int:conversation_id>/delete', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def chat_delete(sender_id):
|
def chat_delete(conversation_id):
|
||||||
sender = User.query.get_or_404(sender_id)
|
conversation = Conversation.query.get_or_404(conversation_id)
|
||||||
ChatMessage.query.filter(or_(
|
if current_user.is_admin() or current_user.is_member(current_user):
|
||||||
and_(ChatMessage.recipient_id == current_user.id, ChatMessage.sender_id == sender.id),
|
Report.query.filter(Report.suspect_conversation_id == conversation.id).delete()
|
||||||
and_(ChatMessage.recipient_id == sender.id, ChatMessage.sender_id == current_user.id))
|
db.session.delete(conversation)
|
||||||
).delete()
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Conversation deleted'))
|
flash(_('Conversation deleted'))
|
||||||
return redirect(url_for('chat.chat_home'))
|
return redirect(url_for('chat.chat_home'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/chat/<int:sender_id>/block_instance', methods=['GET', 'POST'])
|
@bp.route('/chat/<int:instance_id>/block_instance', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def block_instance(sender_id):
|
def block_instance(instance_id):
|
||||||
sender = User.query.get_or_404(sender_id)
|
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=instance_id).first()
|
||||||
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=sender.instance_id).first()
|
|
||||||
if not existing:
|
if not existing:
|
||||||
db.session.add(InstanceBlock(user_id=current_user.id, instance_id=sender.instance_id))
|
db.session.add(InstanceBlock(user_id=current_user.id, instance_id=instance_id))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Instance blocked.'))
|
flash(_('Instance blocked.'))
|
||||||
return redirect(url_for('chat.chat_home'))
|
return redirect(url_for('chat.chat_home'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/chat/<int:sender_id>/report', methods=['GET', 'POST'])
|
@bp.route('/chat/<int:conversation_id>/report', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def chat_report(sender_id):
|
def chat_report(conversation_id):
|
||||||
sender = User.query.get_or_404(sender_id)
|
conversation = Conversation.query.get_or_404(conversation_id)
|
||||||
form = ReportUserForm()
|
if current_user.is_admin() or current_user.is_member(current_user):
|
||||||
if not sender.banned:
|
form = ReportConversationForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=0, reporter_id=current_user.id, suspect_user_id=sender.id)
|
type=4, reporter_id=current_user.id, suspect_conversation_id=conversation_id)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify site admin
|
# Notify site admin
|
||||||
|
@ -175,20 +153,18 @@ def chat_report(sender_id):
|
||||||
author_id=current_user.id)
|
author_id=current_user.id)
|
||||||
db.session.add(notify)
|
db.session.add(notify)
|
||||||
admin.unread_notifications += 1
|
admin.unread_notifications += 1
|
||||||
sender.reports += 1
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# todo: federate report to originating instance
|
# todo: federate report to originating instance
|
||||||
if not sender.is_local() and form.report_remote.data:
|
if form.report_remote.data:
|
||||||
...
|
...
|
||||||
|
|
||||||
flash(_('%(user_name)s has been reported, thank you!', user_name=sender.link()))
|
flash(_('This conversation has been reported, thank you!'))
|
||||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{sender.link()}'
|
return redirect(url_for('chat.chat_home', conversation_id=conversation_id))
|
||||||
return redirect(goto)
|
|
||||||
elif request.method == 'GET':
|
elif request.method == 'GET':
|
||||||
form.report_remote.data = True
|
form.report_remote.data = True
|
||||||
|
|
||||||
return render_template('user/user_report.html', title=_('Report user'), form=form, user=sender,
|
return render_template('chat/report.html', title=_('Report conversation'), form=form, conversation=conversation,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id())
|
joined_communities=joined_communities(current_user.get_id())
|
||||||
)
|
)
|
||||||
|
|
82
app/chat/util.py
Normal file
82
app/chat/util.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
from flask import flash, current_app
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_babel import _
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.activitypub.signature import post_request
|
||||||
|
from app.models import User, ChatMessage, Notification, utcnow, Conversation
|
||||||
|
from app.utils import allowlist_html, shorten_string, gibberish, markdown_to_html
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(form, conversation_id: int) -> ChatMessage:
|
||||||
|
conversation = Conversation.query.get(conversation_id)
|
||||||
|
reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id,
|
||||||
|
body=form.message.data, body_html=allowlist_html(markdown_to_html(form.message.data)))
|
||||||
|
for recipient in conversation.members:
|
||||||
|
if recipient.id != current_user.id:
|
||||||
|
if recipient.is_local():
|
||||||
|
# Notify local recipient
|
||||||
|
notify = Notification(title=shorten_string('New message from ' + current_user.display_name()),
|
||||||
|
url='/chat/' + str(conversation_id),
|
||||||
|
user_id=recipient.id,
|
||||||
|
author_id=current_user.id)
|
||||||
|
db.session.add(notify)
|
||||||
|
recipient.unread_notifications += 1
|
||||||
|
db.session.add(reply)
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
db.session.add(reply)
|
||||||
|
db.session.commit()
|
||||||
|
# Federate reply
|
||||||
|
reply_json = {
|
||||||
|
"actor": current_user.profile_id(),
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||||
|
"object": {
|
||||||
|
"attributedTo": current_user.profile_id(),
|
||||||
|
"content": reply.body_html,
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/private_message/{reply.id}",
|
||||||
|
"mediaType": "text/html",
|
||||||
|
"published": utcnow().isoformat() + 'Z', # Lemmy is inconsistent with the date format they use
|
||||||
|
"source": {
|
||||||
|
"content": reply.body,
|
||||||
|
"mediaType": "text/markdown"
|
||||||
|
},
|
||||||
|
"to": [
|
||||||
|
recipient.profile_id()
|
||||||
|
],
|
||||||
|
"type": "ChatMessage"
|
||||||
|
},
|
||||||
|
"to": [
|
||||||
|
recipient.profile_id()
|
||||||
|
],
|
||||||
|
"type": "Create"
|
||||||
|
}
|
||||||
|
success = post_request(recipient.ap_inbox_url, reply_json, current_user.private_key,
|
||||||
|
current_user.profile_id() + '#main-key')
|
||||||
|
if not success:
|
||||||
|
flash(_('Message failed to send to %(name)s.', name=recipient.link()), 'error')
|
||||||
|
|
||||||
|
flash(_('Message sent.'))
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
def find_existing_conversation(recipient, sender):
|
||||||
|
sql = """SELECT
|
||||||
|
c.id AS conversation_id,
|
||||||
|
c.created_at AS conversation_created_at,
|
||||||
|
c.updated_at AS conversation_updated_at,
|
||||||
|
cm1.user_id AS user1_id,
|
||||||
|
cm2.user_id AS user2_id
|
||||||
|
FROM
|
||||||
|
public.conversation AS c
|
||||||
|
JOIN
|
||||||
|
public.conversation_member AS cm1 ON c.id = cm1.conversation_id
|
||||||
|
JOIN
|
||||||
|
public.conversation_member AS cm2 ON c.id = cm2.conversation_id
|
||||||
|
WHERE
|
||||||
|
cm1.user_id = :user_id_1 AND
|
||||||
|
cm2.user_id = :user_id_2 AND
|
||||||
|
cm1.user_id <> cm2.user_id;"""
|
||||||
|
ec = db.session.execute(text(sql), {'user_id_1': recipient.id, 'user_id_2': sender.id}).fetchone()
|
||||||
|
return Conversation.query.get(ec[0]) if ec else None
|
|
@ -256,75 +256,8 @@ def list_files(directory):
|
||||||
|
|
||||||
@bp.route('/test')
|
@bp.route('/test')
|
||||||
def test():
|
def test():
|
||||||
|
u = User.query.get(1)
|
||||||
instance = Instance.query.get(3)
|
return 'ok'
|
||||||
if instance.updated_at < utcnow() - timedelta(days=7):
|
|
||||||
try:
|
|
||||||
response = get_request(f'https://{instance.domain}/api/v3/site')
|
|
||||||
except:
|
|
||||||
response = None
|
|
||||||
|
|
||||||
if response and response.status_code == 200:
|
|
||||||
try:
|
|
||||||
instance_data = response.json()
|
|
||||||
except:
|
|
||||||
instance_data = None
|
|
||||||
finally:
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
if instance_data:
|
|
||||||
if 'admins' in instance_data:
|
|
||||||
admin_profile_ids = []
|
|
||||||
for admin in instance_data['admins']:
|
|
||||||
admin_profile_ids.append(admin['person']['actor_id'].lower())
|
|
||||||
user = find_actor_or_create(admin['person']['actor_id'])
|
|
||||||
if user and not instance.user_is_admin(user.id):
|
|
||||||
new_instance_role = InstanceRole(instance_id=instance.id, user_id=user.id, role='admin')
|
|
||||||
db.session.add(new_instance_role)
|
|
||||||
db.session.commit()
|
|
||||||
# remove any InstanceRoles that are no longer part of instance-data['admins']
|
|
||||||
for instance_admin in InstanceRole.query.filter_by(instance_id=instance.id):
|
|
||||||
if instance_admin.user.profile_id() not in admin_profile_ids:
|
|
||||||
db.session.query(InstanceRole).filter(
|
|
||||||
InstanceRole.user_id == instance_admin.user.id,
|
|
||||||
InstanceRole.instance_id == instance.id,
|
|
||||||
InstanceRole.role == 'admin').delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return 'Ok'
|
|
||||||
|
|
||||||
return ''
|
|
||||||
retval = ''
|
|
||||||
for user in User.query.all():
|
|
||||||
filesize = user.filesize()
|
|
||||||
num_content = user.num_content()
|
|
||||||
if filesize > 0 and num_content > 0:
|
|
||||||
retval += f'{user.id},"{user.ap_id}",{filesize},{num_content}\n'
|
|
||||||
return retval
|
|
||||||
|
|
||||||
return ''
|
|
||||||
deleted = 0
|
|
||||||
for user in User.query.all():
|
|
||||||
if not user.is_local():
|
|
||||||
if user.cover_id:
|
|
||||||
file = user.cover
|
|
||||||
if file.file_path and file.thumbnail_path:
|
|
||||||
if os.path.exists(file.file_path):
|
|
||||||
os.unlink(file.file_path)
|
|
||||||
deleted += 1
|
|
||||||
file.file_path = ''
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return str(deleted) + ' done'
|
|
||||||
|
|
||||||
return current_app.config['SERVER_NAME']
|
|
||||||
|
|
||||||
#ip = request.headers.get('X-Forwarded-For') or request.remote_addr
|
|
||||||
#if ',' in ip: # Remove all but first ip addresses
|
|
||||||
# ip = ip[:ip.index(',')].strip()
|
|
||||||
#return ip
|
|
||||||
|
|
||||||
|
|
||||||
def verification_warning():
|
def verification_warning():
|
||||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime, timedelta, date, timezone
|
||||||
from time import time
|
from time import time
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from flask import current_app, escape, url_for
|
from flask import current_app, escape, url_for, render_template_string
|
||||||
from flask_login import UserMixin, current_user
|
from flask_login import UserMixin, current_user
|
||||||
from sqlalchemy import or_, text
|
from sqlalchemy import or_, text
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
@ -88,6 +88,62 @@ class InstanceBlock(db.Model):
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class Conversation(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
||||||
|
reported = db.Column(db.Boolean, default=False)
|
||||||
|
read = db.Column(db.Boolean, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
initiator = db.relationship('User', backref=db.backref('conversations_initiated', lazy='dynamic'),
|
||||||
|
foreign_keys=[user_id])
|
||||||
|
messages = db.relationship('ChatMessage', backref=db.backref('conversation'), cascade='all,delete',
|
||||||
|
lazy='dynamic')
|
||||||
|
|
||||||
|
def member_names(self, user_id):
|
||||||
|
retval = []
|
||||||
|
for member in self.members:
|
||||||
|
if member.id != user_id:
|
||||||
|
retval.append(member.display_name())
|
||||||
|
return ', '.join(retval)
|
||||||
|
|
||||||
|
def is_member(self, user):
|
||||||
|
for member in self.members:
|
||||||
|
if member.id == user.id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def instances(self):
|
||||||
|
retval = []
|
||||||
|
for member in self.members:
|
||||||
|
if member.instance.id != 1 and member.instance not in retval:
|
||||||
|
retval.append(member.instance)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
conversation_member = db.Table('conversation_member',
|
||||||
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
|
||||||
|
db.Column('conversation_id', db.Integer, db.ForeignKey('conversation.id')),
|
||||||
|
db.PrimaryKeyConstraint('user_id', 'conversation_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
||||||
|
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
||||||
|
conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'), index=True)
|
||||||
|
body = db.Column(db.Text)
|
||||||
|
body_html = db.Column(db.Text)
|
||||||
|
reported = db.Column(db.Boolean, default=False)
|
||||||
|
read = db.Column(db.Boolean, default=False)
|
||||||
|
encrypted = db.Column(db.String(15))
|
||||||
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
sender = db.relationship('User', foreign_keys=[sender_id])
|
||||||
|
|
||||||
|
|
||||||
class File(db.Model):
|
class File(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
file_path = db.Column(db.String(255))
|
file_path = db.Column(db.String(255))
|
||||||
|
@ -386,6 +442,7 @@ class User(UserMixin, db.Model):
|
||||||
avatar = db.relationship('File', lazy='joined', foreign_keys=[avatar_id], single_parent=True, cascade="all, delete-orphan")
|
avatar = db.relationship('File', lazy='joined', foreign_keys=[avatar_id], single_parent=True, cascade="all, delete-orphan")
|
||||||
cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan")
|
cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan")
|
||||||
instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id])
|
instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id])
|
||||||
|
conversations = db.relationship('Conversation', lazy='dynamic', secondary=conversation_member, backref=db.backref('members', lazy='joined'))
|
||||||
|
|
||||||
ap_id = db.Column(db.String(255), index=True) # e.g. username@server
|
ap_id = db.Column(db.String(255), index=True) # e.g. username@server
|
||||||
ap_profile_id = db.Column(db.String(255), index=True) # e.g. https://server/u/username
|
ap_profile_id = db.Column(db.String(255), index=True) # e.g. https://server/u/username
|
||||||
|
@ -959,20 +1016,6 @@ class PostReplyVote(db.Model):
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
|
||||||
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
|
||||||
body = db.Column(db.Text)
|
|
||||||
body_html = db.Column(db.Text)
|
|
||||||
reported = db.Column(db.Boolean, default=False)
|
|
||||||
read = db.Column(db.Boolean, default=False)
|
|
||||||
encrypted = db.Column(db.String(15))
|
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
|
||||||
|
|
||||||
sender = db.relationship('User', foreign_keys=[sender_id])
|
|
||||||
|
|
||||||
|
|
||||||
# save every activity to a log, to aid debugging
|
# save every activity to a log, to aid debugging
|
||||||
class ActivityPubLog(db.Model):
|
class ActivityPubLog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -1029,18 +1072,19 @@ class Report(db.Model):
|
||||||
reasons = db.Column(db.String(256))
|
reasons = db.Column(db.String(256))
|
||||||
description = db.Column(db.String(256))
|
description = db.Column(db.String(256))
|
||||||
status = db.Column(db.Integer, default=0)
|
status = db.Column(db.Integer, default=0)
|
||||||
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community
|
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
|
||||||
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
||||||
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
||||||
|
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
updated = db.Column(db.DateTime, default=utcnow)
|
updated = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
# textual representation of self.type
|
# textual representation of self.type
|
||||||
def type_text(self):
|
def type_text(self):
|
||||||
types = ('User', 'Post', 'Comment', 'Community')
|
types = ('User', 'Post', 'Comment', 'Community', 'Conversation')
|
||||||
if self.type is None:
|
if self.type is None:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||||
{{ form.csrf_token() }}
|
{{ form.csrf_token() }}
|
||||||
{{ render_field(form.about) }}
|
{{ render_field(form.about) }}
|
||||||
|
{{ render_field(form.email) }}
|
||||||
{{ render_field(form.matrix_user_id) }}
|
{{ render_field(form.matrix_user_id) }}
|
||||||
{% if user.avatar_id %}
|
{% if user.avatar_id %}
|
||||||
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
||||||
|
@ -30,6 +31,8 @@
|
||||||
{{ render_field(form.banner_file) }}
|
{{ render_field(form.banner_file) }}
|
||||||
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
||||||
{{ render_field(form.bot) }}
|
{{ render_field(form.bot) }}
|
||||||
|
{{ render_field(form.verified) }}
|
||||||
|
{{ render_field(form.banned) }}
|
||||||
{{ render_field(form.newsletter) }}
|
{{ render_field(form.newsletter) }}
|
||||||
{{ render_field(form.nsfw) }}
|
{{ render_field(form.nsfw) }}
|
||||||
{{ render_field(form.nsfl) }}
|
{{ render_field(form.nsfl) }}
|
||||||
|
|
|
@ -35,9 +35,11 @@
|
||||||
<td>{{ report.reasons }}</td>
|
<td>{{ report.reasons }}</td>
|
||||||
<td>{{ report.description }}</td>
|
<td>{{ report.description }}</td>
|
||||||
<td>{{ report.type_text() }}</td>
|
<td>{{ report.type_text() }}</td>
|
||||||
<td>{{ report.created_at }}</td>
|
<td>{{ moment(report.created_at).fromNow() }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if report.suspect_post_reply_id %}
|
{% if report.suspect_conversation_id %}
|
||||||
|
<a href="/chat/{{ report.suspect_conversation_id }}#message">View</a>
|
||||||
|
{% elif report.suspect_post_reply_id %}
|
||||||
<a href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a>
|
<a href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a>
|
||||||
{% elif report.suspect_post_id %}
|
{% elif report.suspect_post_id %}
|
||||||
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
||||||
|
|
20
app/templates/chat/blocked.html
Normal file
20
app/templates/chat/blocked.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Sorry') }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{{ _('You have blocked this person or they have blocked you.') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -10,17 +10,21 @@
|
||||||
<div class="col col-login mx-auto">
|
<div class="col col-login mx-auto">
|
||||||
<div class="card mt-5">
|
<div class="card mt-5">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<div class="card-title">{{ _('Options for conversation with "%(sender)s"', sender=sender.link()) }}</div>
|
<div class="card-title">{{ _('Options for conversation with "%(member_names)s"', member_names=conversation.member_names(current_user.id)) }}</div>
|
||||||
<ul class="option_list">
|
<ul class="option_list">
|
||||||
<li><a href="{{ url_for('chat.chat_delete', sender_id=sender.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
<li><a href="{{ url_for('chat.chat_delete', conversation_id=conversation.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||||
{{ _('Delete conversation') }}</a></li>
|
{{ _('Delete conversation') }}</a></li>
|
||||||
<li><a href="{{ url_for('user.block_profile', actor=sender.link()) }}" class="no-underline"><span class="fe fe-block"></span>
|
{% for member in conversation.members %}
|
||||||
{{ _('Block @%(author_name)s', author_name=sender.display_name()) }}</a></li>
|
{% if member.id != current_user.id %}
|
||||||
{% if sender.instance_id and sender.instance_id != 1 %}
|
<li><a href="{{ url_for('user.block_profile', actor=member.link()) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||||
<li><a href="{{ url_for('chat.block_instance', sender_id=sender.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
{{ _('Block @%(author_name)s', author_name=member.display_name()) }}</a></li>
|
||||||
{{ _("Block everything from instance: %(name)s", name=sender.instance.domain) }}</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('chat.chat_report', sender_id=sender.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
{% endfor %}
|
||||||
|
{% for instance in conversation.instances() %}
|
||||||
|
<li><a href="{{ url_for('chat.block_instance', instance_id=instance.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||||
|
{{ _("Block chats and posts from instance: %(name)s", name=instance.domain) }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li><a href="{{ url_for('chat.chat_report', conversation_id=conversation.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||||
{{ _('Report to moderators') }}</a></li>
|
{{ _('Report to moderators') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>{{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}</p>
|
<p>{{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}</p>
|
||||||
|
|
|
@ -5,6 +5,22 @@
|
||||||
{% endif %} %}
|
{% endif %} %}
|
||||||
{% from 'bootstrap/form.html' import render_form %}
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% macro conversation_members(conversation) %}
|
||||||
|
{% if len(conversation.members) == 2 %}
|
||||||
|
{% for member in conversation.members %}
|
||||||
|
{% if member.id != current_user.id %}
|
||||||
|
<img src="{{ member.avatar_thumbnail() }}" loading="lazy" /> {{ member.display_name() }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% for member in conversation.members %}
|
||||||
|
{% if member.id != current_user.id %}
|
||||||
|
{{ member.display_name() }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -15,25 +31,17 @@
|
||||||
<li class="breadcrumb-item active">{{ _('Chat') }}</li>
|
<li class="breadcrumb-item active">{{ _('Chat') }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="mt-2">{{ _('Chat') }}</h1>
|
<div class="row mt-3">
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-2 d-none d-md-block sender_list">
|
<div class="col col-md-2 d-none d-md-block sender_list">
|
||||||
<h3>{{ _('People') }}</h3>
|
<h3>{{ _('People') }}</h3>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for sender in senders %}
|
{% for conversation in conversations %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
{% if other_party %}
|
{% if conversation.id == current_conversation %}
|
||||||
{% if other_party.id != sender.id %}
|
{{ conversation_members(conversation) }}
|
||||||
<a href="{{ url_for('chat.chat_home', sender_id=sender.id) }}">{% if sender.avatar_image() %}<img src="{{ sender.avatar_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />{% endif %}
|
|
||||||
{{ sender.link() }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if sender.avatar_image() %}<img src="{{ sender.avatar_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />{% endif %}
|
<a href="{{ url_for('chat.chat_home', conversation_id=conversation.id) }}">
|
||||||
{{ sender.link() }}
|
{{ conversation_members(conversation) }}
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('chat.chat_home', sender_id=sender.id) }}">{% if sender.avatar_image() %}<img src="{{ sender.avatar_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />{% endif %}
|
|
||||||
{{ sender.link() }}
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
@ -41,24 +49,24 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-10">
|
<div class="col col-md-10">
|
||||||
{% if other_party %}
|
{% if messages %}
|
||||||
<h3 class="d-none d-md-inline">{{ _('Messages with %(name)s', name=other_party.display_name()) }}</h3>
|
<h3 class="d-none d-md-inline">{{ _('Messages with %(name)s', name=conversation.member_names(current_user.id)) }}</h3>
|
||||||
<h3 class="d-md-none">{{ _('Messages with: ') }}
|
<h3 class="d-md-none">{{ _('Messages with: ') }}
|
||||||
<select id="changeSender">{% for sender in senders %}
|
<select id="changeSender">{% for conversation in conversations %}
|
||||||
<option value="{{ sender.id }}" {{ 'selected' if sender.id == other_party.id }}>{{ sender.link() }}</option>
|
<option value="{{ conversation.id }}" {{ 'selected' if conversation.id == current_conversation }}>{{ conversation.member_names(current_user.id) }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select></h3>
|
</select></h3>
|
||||||
<div class="conversation">
|
<div class="conversation mt-3">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div id="message_{{ message.id }}" class="card message {{ 'from_other_party' if message.sender_id != current_user.id else 'from_me' }}">
|
<div id="message_{{ message.id }}" class="card message {{ 'from_other_party' if message.sender_id != current_user.id else 'from_me' }}">
|
||||||
<div class="message_body">
|
<div class="message_body">
|
||||||
<span class="message_created_at text-muted small">{{ moment(message.created_at).fromNow(refresh=True) }}</span>
|
<span class="message_created_at text-muted small">{{ moment(message.created_at).fromNow(refresh=True) }}</span>
|
||||||
<span class="message_sender">{{ message.sender.display_name() }}</span>: {{ message.body_html|safe }}
|
<span class="message_sender"><a href="/u/{{ message.sender.link() }}">{{ message.sender.display_name() }}</a></span>: {{ message.body_html|safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
<a class="conversation_options btn btn-outline-secondary" href="{{ url_for('chat.chat_options', sender_id=other_party.id) }}" class="btn btn-outline-secondary">{{ _('Options') }}</a>
|
<a class="conversation_options btn btn-outline-secondary" href="{{ url_for('chat.chat_options', conversation_id=current_conversation) }}" class="btn btn-outline-secondary">{{ _('Options') }}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
21
app/templates/chat/denied.html
Normal file
21
app/templates/chat/denied.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Sorry') }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{{ _('You have not been using PieFed long enough to be allowed to send messages to people.') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
21
app/templates/chat/new_message.html
Normal file
21
app/templates/chat/new_message.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('New message to "%(recipient_name)s"', recipient_name=recipient.link()) }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
21
app/templates/chat/report.html
Normal file
21
app/templates/chat/report.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Report conversation with "%(member_names)s"', member_names=conversation.member_names(current_user.id)) }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -41,12 +41,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="profile_action_buttons">
|
<div class="profile_action_buttons">
|
||||||
{% if not current_user.created_recently() and current_user.reputation > 10 %}
|
|
||||||
<a class="btn btn-primary" href="{{ url_for('chat.new_message', to=user.id) }}" rel="nofollow" aria-label="{{ _('Send message') }}">{{ _('Send message') }}</a>
|
<a class="btn btn-primary" href="{{ url_for('chat.new_message', to=user.id) }}" rel="nofollow" aria-label="{{ _('Send message') }}">{{ _('Send message') }}</a>
|
||||||
{% if user.matrix_user_id %}
|
{% if user.matrix_user_id %}
|
||||||
<a class="btn btn-primary" href="https://matrix.to/#/{{ user.matrix_user_id }}" rel="nofollow" aria-label="{{ _('Send message with matrix chat') }}">{{ _('Send message using Matrix') }}</a>
|
<a class="btn btn-primary" href="https://matrix.to/#/{{ user.matrix_user_id }}" rel="nofollow" aria-label="{{ _('Send message with matrix chat') }}">{{ _('Send message using Matrix') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if current_user.id != user.id %}
|
{% if current_user.id != user.id %}
|
||||||
{% if current_user.has_blocked_user(user.id) %}
|
{% if current_user.has_blocked_user(user.id) %}
|
||||||
<a class="btn btn-primary" href="{{ url_for('user.unblock_profile', actor=user.link()) }}" rel="nofollow">{{ _('Unblock user') }}</a>
|
<a class="btn btn-primary" href="{{ url_for('user.unblock_profile', actor=user.link()) }}" rel="nofollow">{{ _('Unblock user') }}</a>
|
||||||
|
|
|
@ -389,7 +389,7 @@ def user_cookie_banned() -> bool:
|
||||||
return cookie is not None
|
return cookie is not None
|
||||||
|
|
||||||
|
|
||||||
@cache.memoize(timeout=300)
|
@cache.memoize(timeout=30)
|
||||||
def banned_ip_addresses() -> List[str]:
|
def banned_ip_addresses() -> List[str]:
|
||||||
ips = IpBan.query.all()
|
ips = IpBan.query.all()
|
||||||
return [ip.ip_address for ip in ips]
|
return [ip.ip_address for ip in ips]
|
||||||
|
|
34
migrations/versions/8ca0f0789040_chat_reporting.py
Normal file
34
migrations/versions/8ca0f0789040_chat_reporting.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"""chat reporting
|
||||||
|
|
||||||
|
Revision ID: 8ca0f0789040
|
||||||
|
Revises: b4f7322082f4
|
||||||
|
Create Date: 2024-02-19 14:58:13.481708
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8ca0f0789040'
|
||||||
|
down_revision = 'b4f7322082f4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('suspect_conversation_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key(None, 'conversation', ['suspect_conversation_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.drop_column('suspect_conversation_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
61
migrations/versions/b4f7322082f4_chat_conversations.py
Normal file
61
migrations/versions/b4f7322082f4_chat_conversations.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
"""chat conversations
|
||||||
|
|
||||||
|
Revision ID: b4f7322082f4
|
||||||
|
Revises: fe1e3fbf5b9d
|
||||||
|
Create Date: 2024-02-18 14:54:20.090872
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b4f7322082f4'
|
||||||
|
down_revision = 'fe1e3fbf5b9d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('conversation',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('reported', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('read', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('conversation', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_conversation_user_id'), ['user_id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('conversation_member',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('conversation_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['conversation_id'], ['conversation.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'conversation_id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('chat_message', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('conversation_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_index(batch_op.f('ix_chat_message_conversation_id'), ['conversation_id'], unique=False)
|
||||||
|
batch_op.create_foreign_key(None, 'conversation', ['conversation_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('chat_message', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.drop_index(batch_op.f('ix_chat_message_conversation_id'))
|
||||||
|
batch_op.drop_column('conversation_id')
|
||||||
|
|
||||||
|
op.drop_table('conversation_member')
|
||||||
|
with op.batch_alter_table('conversation', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_conversation_user_id'))
|
||||||
|
|
||||||
|
op.drop_table('conversation')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue