diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 6f581881..bf733591 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -83,7 +83,7 @@ def nodeinfo(): @bp.route('/.well-known/host-meta') @cache.cached(timeout=600) def host_meta(): - resp = make_response(f'\n\n\n') + resp = make_response('\n\n\n') 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 diff --git a/app/admin/forms.py b/app/admin/forms.py index f92a68f4..e933745c 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -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')) diff --git a/app/admin/routes.py b/app/admin/routes.py index f4297102..f02862ef 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -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 diff --git a/app/auth/routes.py b/app/auth/routes.py index 3e7bbf05..7fb5e87d 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -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'))) diff --git a/app/chat/forms.py b/app/chat/forms.py index fe9afd89..f23a4581 100644 --- a/app/chat/forms.py +++ b/app/chat/forms.py @@ -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) diff --git a/app/chat/routes.py b/app/chat/routes.py index e21ce07a..2e1f887e 100644 --- a/app/chat/routes.py +++ b/app/chat/routes.py @@ -1,170 +1,148 @@ -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/', methods=['GET', 'POST']) +@bp.route('/chat/', 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() - 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}')) + reply = send_message(form, conversation_id) + return redirect(url_for('chat.chat_home', conversation_id=conversation_id, _anchor=f'message_{reply.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() - if messages: - if messages[0].sender_id == current_user.id: - other_party = User.query.get(messages[0].recipient_id) - else: - other_party = User.query.get(messages[0].sender_id) - else: - other_party = None - form.recipient_id.data = messages_with + 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: - 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}" + 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) + else: + other_party = User.query.get(messages[0].sender_id) + else: + other_party = None + else: + messages = [] + other_party = None + + 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, - moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()), - site=g.site) + 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) @bp.route('/chat//new', methods=['GET', 'POST']) @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//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//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//delete', methods=['GET', 'POST']) +@bp.route('/chat//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() - db.session.commit() - flash(_('Conversation deleted')) +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//block_instance', methods=['GET', 'POST']) +@bp.route('/chat//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//report', methods=['GET', 'POST']) +@bp.route('/chat//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, - moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) - ) + 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()) + ) diff --git a/app/chat/util.py b/app/chat/util.py new file mode 100644 index 00000000..20ecafd1 --- /dev/null +++ b/app/chat/util.py @@ -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 diff --git a/app/main/routes.py b/app/main/routes.py index 51c9c18a..61ce8caf 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -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(): diff --git a/app/models.py b/app/models.py index e226bb4e..19e66719 100644 --- a/app/models.py +++ b/app/models.py @@ -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: diff --git a/app/templates/admin/edit_user.html b/app/templates/admin/edit_user.html index f71b478b..5aa8c2c8 100644 --- a/app/templates/admin/edit_user.html +++ b/app/templates/admin/edit_user.html @@ -18,6 +18,7 @@
{{ form.csrf_token() }} {{ render_field(form.about) }} + {{ render_field(form.email) }} {{ render_field(form.matrix_user_id) }} {% if user.avatar_id %} @@ -30,6 +31,8 @@ {{ render_field(form.banner_file) }} Provide a wide image - letterbox orientation. {{ render_field(form.bot) }} + {{ render_field(form.verified) }} + {{ render_field(form.banned) }} {{ render_field(form.newsletter) }} {{ render_field(form.nsfw) }} {{ render_field(form.nsfl) }} diff --git a/app/templates/admin/reports.html b/app/templates/admin/reports.html index 67af4e06..f66a1fd3 100644 --- a/app/templates/admin/reports.html +++ b/app/templates/admin/reports.html @@ -35,9 +35,11 @@ {{ report.reasons }} {{ report.description }} {{ report.type_text() }} - {{ report.created_at }} + {{ moment(report.created_at).fromNow() }} - {% if report.suspect_post_reply_id %} + {% if report.suspect_conversation_id %} + View + {% elif report.suspect_post_reply_id %} View {% elif report.suspect_post_id %} View diff --git a/app/templates/chat/blocked.html b/app/templates/chat/blocked.html new file mode 100644 index 00000000..93ecd79b --- /dev/null +++ b/app/templates/chat/blocked.html @@ -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 %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/chat/chat_options.html b/app/templates/chat/chat_options.html index e1faa52a..6ed7b88c 100644 --- a/app/templates/chat/chat_options.html +++ b/app/templates/chat/chat_options.html @@ -10,17 +10,21 @@