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')
|
||||
@cache.cached(timeout=600)
|
||||
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'
|
||||
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 = find_actor_or_create(recipient_ap_id)
|
||||
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"
|
||||
else:
|
||||
# Save ChatMessage to DB
|
||||
|
|
|
@ -98,10 +98,13 @@ class EditTopicForm(FlaskForm):
|
|||
|
||||
class EditUserForm(FlaskForm):
|
||||
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)])
|
||||
profile_file = FileField(_l('Avatar image'))
|
||||
banner_file = FileField(_l('Top banner image'))
|
||||
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'))
|
||||
ignore_bots = BooleanField(_l('Hide posts by bots'))
|
||||
nsfw = BooleanField(_l('Show NSFW posts'))
|
||||
|
|
|
@ -484,9 +484,12 @@ def admin_user_edit(user_id):
|
|||
user = User.query.get_or_404(user_id)
|
||||
if form.validate_on_submit():
|
||||
user.about = form.about.data
|
||||
user.email = form.email.data
|
||||
user.about_html = markdown_to_html(form.about.data)
|
||||
user.matrix_user_id = form.matrix_user_id.data
|
||||
user.bot = form.bot.data
|
||||
user.verified = form.verified.data
|
||||
user.banned = form.banned.data
|
||||
profile_file = request.files['profile_file']
|
||||
if profile_file and profile_file.filename != '':
|
||||
# remove old avatar
|
||||
|
@ -528,9 +531,12 @@ def admin_user_edit(user_id):
|
|||
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')
|
||||
form.about.data = user.about
|
||||
form.email.data = user.email
|
||||
form.matrix_user_id.data = user.matrix_user_id
|
||||
form.newsletter.data = user.newsletter
|
||||
form.bot.data = user.bot
|
||||
form.verified.data = user.verified
|
||||
form.banned.data = user.banned
|
||||
form.ignore_bots.data = user.ignore_bots
|
||||
form.nsfw.data = user.show_nsfw
|
||||
form.nsfl.data = user.show_nsfl
|
||||
|
|
|
@ -38,7 +38,7 @@ def login():
|
|||
return redirect(url_for('auth.login'))
|
||||
flash(_('Invalid password'))
|
||||
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')
|
||||
|
||||
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 app import db
|
||||
from app.utils import MultiCheckboxField
|
||||
|
||||
|
||||
class AddReply(FlaskForm):
|
||||
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'))
|
||||
|
||||
|
||||
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
|
||||
from flask import request, flash, json, url_for, current_app, redirect, g, abort
|
||||
from flask_login import login_required, current_user
|
||||
from flask_babel import _
|
||||
from sqlalchemy import desc, or_, and_, text
|
||||
|
||||
from app import db, celery
|
||||
from app.activitypub.signature import post_request
|
||||
from app.chat.forms import AddReply
|
||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
||||
User, Instance, File, Report, Topic, UserRegistration, ChatMessage, Notification, InstanceBlock
|
||||
from app.chat.forms import AddReply, ReportConversationForm
|
||||
from app.chat.util import send_message, find_existing_conversation
|
||||
from app.models import Site, User, Report, ChatMessage, Notification, InstanceBlock, Conversation, conversation_member
|
||||
from app.user.forms import ReportUserForm
|
||||
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
||||
moderating_communities, joined_communities, finalize_user_setup, theme_list, allowlist_html, shorten_string
|
||||
from app.utils import render_template, moderating_communities, joined_communities
|
||||
from app.chat import bp
|
||||
|
||||
|
||||
@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
|
||||
def chat_home(sender_id=None):
|
||||
def chat_home(conversation_id=None):
|
||||
form = AddReply()
|
||||
if form.validate_on_submit():
|
||||
recipient = User.query.get(form.recipient_id.data)
|
||||
reply = ChatMessage(sender_id=current_user.id, recipient_id=recipient.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()
|
||||
reply = send_message(form, conversation_id)
|
||||
return redirect(url_for('chat.chat_home', conversation_id=conversation_id, _anchor=f'message_{reply.id}'))
|
||||
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 remote server. Try again later.'), 'error')
|
||||
|
||||
return redirect(url_for('chat.chat_home', sender_id=recipient.id, _anchor=f'message_{reply.id}'))
|
||||
conversations = Conversation.query.join(conversation_member,
|
||||
conversation_member.c.conversation_id == Conversation.id). \
|
||||
filter(conversation_member.c.user_id == current_user.id).order_by(desc(Conversation.updated_at)).limit(50).all()
|
||||
if conversation_id is None:
|
||||
return redirect(url_for('chat.chat_home', conversation_id=conversations[0].id))
|
||||
else:
|
||||
senders = User.query.filter(User.banned == False).join(ChatMessage, ChatMessage.sender_id == User.id)
|
||||
senders = senders.filter(ChatMessage.recipient_id == current_user.id).order_by(desc(ChatMessage.created_at)).limit(500).all()
|
||||
|
||||
if senders:
|
||||
messages_with = senders[0].id if sender_id is None else sender_id
|
||||
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()
|
||||
conversation = Conversation.query.get_or_404(conversation_id)
|
||||
if not current_user.is_admin() and not conversation.is_member(current_user):
|
||||
abort(400)
|
||||
if conversations:
|
||||
messages = conversation.messages.order_by(ChatMessage.created_at).all()
|
||||
if messages:
|
||||
if messages[0].sender_id == current_user.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)
|
||||
else:
|
||||
other_party = None
|
||||
form.recipient_id.data = messages_with
|
||||
else:
|
||||
messages = []
|
||||
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.commit()
|
||||
current_user.unread_notifications = Notification.query.filter_by(user_id=current_user.id, read=False).count()
|
||||
db.session.commit()
|
||||
|
||||
return render_template('chat/home.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,
|
||||
return render_template('chat/conversation.html', title=_('Chat with %(name)s', name=other_party.display_name()) if other_party else _('Chat'),
|
||||
conversations=conversations, messages=messages, form=form,
|
||||
current_conversation=conversation_id, conversation=conversation,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
site=g.site)
|
||||
|
@ -108,63 +61,88 @@ def chat_home(sender_id=None):
|
|||
@login_required
|
||||
def new_message(to):
|
||||
recipient = User.query.get_or_404(to)
|
||||
existing_conversation = ChatMessage.query.filter(or_(
|
||||
and_(ChatMessage.recipient_id == current_user.id, ChatMessage.sender_id == recipient.id),
|
||||
and_(ChatMessage.recipient_id == recipient.id, ChatMessage.sender_id == current_user.id))
|
||||
).first()
|
||||
if current_user.created_recently() or current_user.reputation < 10 or current_user.banned or not current_user.verified:
|
||||
return redirect(url_for('chat.denied'))
|
||||
if recipient.has_blocked_user(current_user.id) or current_user.has_blocked_user(recipient.id):
|
||||
return redirect(url_for('chat.blocked'))
|
||||
existing_conversation = find_existing_conversation(recipient=recipient, sender=current_user)
|
||||
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.submit.label.text = _('Send')
|
||||
if form.validate_on_submit():
|
||||
flash(_('Message sent'))
|
||||
return redirect(url_for('chat.home', sender_id=recipient.id))
|
||||
conversation = Conversation(user_id=current_user.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
|
||||
def chat_options(sender_id):
|
||||
sender = User.query.get_or_404(sender_id)
|
||||
return render_template('chat/chat_options.html', sender=sender,
|
||||
def denied():
|
||||
return render_template('chat/denied.html')
|
||||
|
||||
|
||||
@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()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
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
|
||||
def chat_delete(sender_id):
|
||||
sender = User.query.get_or_404(sender_id)
|
||||
ChatMessage.query.filter(or_(
|
||||
and_(ChatMessage.recipient_id == current_user.id, ChatMessage.sender_id == sender.id),
|
||||
and_(ChatMessage.recipient_id == sender.id, ChatMessage.sender_id == current_user.id))
|
||||
).delete()
|
||||
def chat_delete(conversation_id):
|
||||
conversation = Conversation.query.get_or_404(conversation_id)
|
||||
if current_user.is_admin() or current_user.is_member(current_user):
|
||||
Report.query.filter(Report.suspect_conversation_id == conversation.id).delete()
|
||||
db.session.delete(conversation)
|
||||
db.session.commit()
|
||||
flash(_('Conversation deleted'))
|
||||
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
|
||||
def block_instance(sender_id):
|
||||
sender = User.query.get_or_404(sender_id)
|
||||
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=sender.instance_id).first()
|
||||
def block_instance(instance_id):
|
||||
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=instance_id).first()
|
||||
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()
|
||||
flash(_('Instance blocked.'))
|
||||
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
|
||||
def chat_report(sender_id):
|
||||
sender = User.query.get_or_404(sender_id)
|
||||
form = ReportUserForm()
|
||||
if not sender.banned:
|
||||
def chat_report(conversation_id):
|
||||
conversation = Conversation.query.get_or_404(conversation_id)
|
||||
if current_user.is_admin() or current_user.is_member(current_user):
|
||||
form = ReportConversationForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
# Notify site admin
|
||||
|
@ -175,20 +153,18 @@ def chat_report(sender_id):
|
|||
author_id=current_user.id)
|
||||
db.session.add(notify)
|
||||
admin.unread_notifications += 1
|
||||
sender.reports += 1
|
||||
db.session.commit()
|
||||
|
||||
# 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()))
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{sender.link()}'
|
||||
return redirect(goto)
|
||||
flash(_('This conversation has been reported, thank you!'))
|
||||
return redirect(url_for('chat.chat_home', conversation_id=conversation_id))
|
||||
elif request.method == 'GET':
|
||||
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()),
|
||||
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')
|
||||
def test():
|
||||
|
||||
instance = Instance.query.get(3)
|
||||
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
|
||||
u = User.query.get(1)
|
||||
return 'ok'
|
||||
|
||||
|
||||
def verification_warning():
|
||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime, timedelta, date, timezone
|
|||
from time import time
|
||||
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 sqlalchemy import or_, text
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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")
|
||||
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])
|
||||
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_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)
|
||||
|
||||
|
||||
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
|
||||
class ActivityPubLog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -1029,18 +1072,19 @@ class Report(db.Model):
|
|||
reasons = db.Column(db.String(256))
|
||||
description = db.Column(db.String(256))
|
||||
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'))
|
||||
suspect_community_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_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)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
# textual representation of self.type
|
||||
def type_text(self):
|
||||
types = ('User', 'Post', 'Comment', 'Community')
|
||||
types = ('User', 'Post', 'Comment', 'Community', 'Conversation')
|
||||
if self.type is None:
|
||||
return ''
|
||||
else:
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.about) }}
|
||||
{{ render_field(form.email) }}
|
||||
{{ render_field(form.matrix_user_id) }}
|
||||
{% if user.avatar_id %}
|
||||
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
||||
|
@ -30,6 +31,8 @@
|
|||
{{ render_field(form.banner_file) }}
|
||||
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
||||
{{ render_field(form.bot) }}
|
||||
{{ render_field(form.verified) }}
|
||||
{{ render_field(form.banned) }}
|
||||
{{ render_field(form.newsletter) }}
|
||||
{{ render_field(form.nsfw) }}
|
||||
{{ render_field(form.nsfl) }}
|
||||
|
|
|
@ -35,9 +35,11 @@
|
|||
<td>{{ report.reasons }}</td>
|
||||
<td>{{ report.description }}</td>
|
||||
<td>{{ report.type_text() }}</td>
|
||||
<td>{{ report.created_at }}</td>
|
||||
<td>{{ moment(report.created_at).fromNow() }}</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>
|
||||
{% elif report.suspect_post_id %}
|
||||
<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="card mt-5">
|
||||
<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">
|
||||
<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>
|
||||
<li><a href="{{ url_for('user.block_profile', actor=sender.link()) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block @%(author_name)s', author_name=sender.display_name()) }}</a></li>
|
||||
{% if sender.instance_id and sender.instance_id != 1 %}
|
||||
<li><a href="{{ url_for('chat.block_instance', sender_id=sender.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Block everything from instance: %(name)s", name=sender.instance.domain) }}</a></li>
|
||||
{% for member in conversation.members %}
|
||||
{% if member.id != current_user.id %}
|
||||
<li><a href="{{ url_for('user.block_profile', actor=member.link()) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block @%(author_name)s', author_name=member.display_name()) }}</a></li>
|
||||
{% 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>
|
||||
</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>
|
||||
|
|
|
@ -5,6 +5,22 @@
|
|||
{% endif %} %}
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
|
@ -15,25 +31,17 @@
|
|||
<li class="breadcrumb-item active">{{ _('Chat') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ _('Chat') }}</h1>
|
||||
<div class="row">
|
||||
<div class="row mt-3">
|
||||
<div class="col col-md-2 d-none d-md-block sender_list">
|
||||
<h3>{{ _('People') }}</h3>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for sender in senders %}
|
||||
{% for conversation in conversations %}
|
||||
<li class="list-group-item">
|
||||
{% if other_party %}
|
||||
{% if other_party.id != sender.id %}
|
||||
<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>
|
||||
{% if conversation.id == current_conversation %}
|
||||
{{ conversation_members(conversation) }}
|
||||
{% else %}
|
||||
{% if sender.avatar_image() %}<img src="{{ sender.avatar_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />{% endif %}
|
||||
{{ sender.link() }}
|
||||
{% 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 href="{{ url_for('chat.chat_home', conversation_id=conversation.id) }}">
|
||||
{{ conversation_members(conversation) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
@ -41,24 +49,24 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="col col-md-10">
|
||||
{% if other_party %}
|
||||
<h3 class="d-none d-md-inline">{{ _('Messages with %(name)s', name=other_party.display_name()) }}</h3>
|
||||
{% if messages %}
|
||||
<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: ') }}
|
||||
<select id="changeSender">{% for sender in senders %}
|
||||
<option value="{{ sender.id }}" {{ 'selected' if sender.id == other_party.id }}>{{ sender.link() }}</option>
|
||||
<select id="changeSender">{% for conversation in conversations %}
|
||||
<option value="{{ conversation.id }}" {{ 'selected' if conversation.id == current_conversation }}>{{ conversation.member_names(current_user.id) }}</option>
|
||||
{% endfor %}
|
||||
</select></h3>
|
||||
<div class="conversation">
|
||||
<div class="conversation mt-3">
|
||||
{% 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 class="message_body">
|
||||
<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>
|
||||
{% endfor %}
|
||||
{{ 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>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if current_user.id != 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>
|
||||
|
|
|
@ -389,7 +389,7 @@ def user_cookie_banned() -> bool:
|
|||
return cookie is not None
|
||||
|
||||
|
||||
@cache.memoize(timeout=300)
|
||||
@cache.memoize(timeout=30)
|
||||
def banned_ip_addresses() -> List[str]:
|
||||
ips = IpBan.query.all()
|
||||
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…
Reference in a new issue