mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
send private messages
This commit is contained in:
parent
e4e02dcc6c
commit
a77de0c883
15 changed files with 431 additions and 43 deletions
|
@ -77,6 +77,9 @@ def create_app(config_class=Config):
|
|||
from app.topic import bp as topic_bp
|
||||
app.register_blueprint(topic_bp)
|
||||
|
||||
from app.chat import bp as chat_bp
|
||||
app.register_blueprint(chat_bp)
|
||||
|
||||
def get_resource_as_string(name, charset='utf-8'):
|
||||
with app.open_resource(name) as f:
|
||||
return f.read().decode(charset)
|
||||
|
|
|
@ -11,7 +11,8 @@ from app.post.routes import continue_discussion, show_post
|
|||
from app.user.routes import show_profile
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, SUBSCRIPTION_MEMBER
|
||||
from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog, Post, \
|
||||
PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow, Site, Notification
|
||||
PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow, Site, Notification, \
|
||||
ChatMessage
|
||||
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
|
||||
post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \
|
||||
lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
|
||||
|
@ -20,7 +21,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
|||
update_post_from_activity, undo_vote, undo_downvote
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
|
||||
can_upvote, can_create, awaken_dormant_instance
|
||||
can_upvote, can_create, awaken_dormant_instance, shorten_string
|
||||
import werkzeug.exceptions
|
||||
|
||||
|
||||
|
@ -373,6 +374,33 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
if request_json['type'] == 'Create':
|
||||
activity_log.activity_type = 'Create'
|
||||
user_ap_id = request_json['object']['attributedTo']
|
||||
if request_json['object']['type'] == 'ChatMessage':
|
||||
activity_log.activity_type = 'Create ChatMessage'
|
||||
sender = find_actor_or_create(user_ap_id)
|
||||
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):
|
||||
activity_log.exception_message = "Sender blocked by recipient"
|
||||
else:
|
||||
# Save ChatMessage to DB
|
||||
encrypted = request_json['object']['encrypted'] if 'encrypted' in request_json['object'] else None
|
||||
new_message = ChatMessage(sender_id=sender.id, recipient_id=recipient.id,
|
||||
body=request_json['object']['source']['content'],
|
||||
body_html=allowlist_html(markdown_to_html(request_json['object']['source']['content'])),
|
||||
encrypted=encrypted)
|
||||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
# Notify recipient
|
||||
notify = Notification(title=shorten_string('New message from ' + sender.display_name()),
|
||||
url=f'/chat/{new_message.id}', user_id=recipient.id,
|
||||
author_id=sender.id)
|
||||
db.session.add(notify)
|
||||
recipient.unread_notifications += 1
|
||||
db.session.commit()
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
try:
|
||||
community_ap_id = request_json['to'][0]
|
||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply
|
||||
|
|
5
app/chat/__init__.py
Normal file
5
app/chat/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('chat', __name__)
|
||||
|
||||
from app.chat import routes
|
14
app/chat/forms.py
Normal file
14
app/chat/forms.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
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'))
|
103
app/chat/routes.py
Normal file
103
app/chat/routes.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import request, flash, json, url_for, current_app, redirect, g
|
||||
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
|
||||
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.chat import bp
|
||||
|
||||
|
||||
@bp.route('/chat', methods=['GET', 'POST'])
|
||||
@bp.route('/chat/<int:sender_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def chat_home(sender_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}'))
|
||||
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
|
||||
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}"
|
||||
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'), 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)
|
|
@ -959,6 +959,20 @@ 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)
|
||||
|
@ -1064,6 +1078,7 @@ class Site(db.Model):
|
|||
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
|
||||
allowlist = db.Column(db.Text, default='')
|
||||
blocklist = db.Column(db.Text, default='')
|
||||
auto_decline_referrers = db.Column(db.Text, default='rdrama.net')
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
last_active = db.Column(db.DateTime, default=utcnow)
|
||||
|
|
|
@ -759,7 +759,7 @@ def post_report(post_id: int):
|
|||
# todo: only notify admins for certain types of report
|
||||
for admin in Site.admins():
|
||||
if admin.id not in already_notified:
|
||||
notify = Notification(title='Suspicious content', url=post.ap_id, user_id=admin.id, author_id=current_user.id)
|
||||
notify = Notification(title='Suspicious content', url='/admin/reports', user_id=admin.id, author_id=current_user.id)
|
||||
db.session.add(notify)
|
||||
admin.unread_notifications += 1
|
||||
db.session.commit()
|
||||
|
@ -861,7 +861,7 @@ def post_reply_report(post_id: int, comment_id: int):
|
|||
# todo: only notify admins for certain types of report
|
||||
for admin in Site.admins():
|
||||
if admin.id not in already_notified:
|
||||
notify = Notification(title='Suspicious content', url=post.ap_id, user_id=admin.id, author_id=current_user.id)
|
||||
notify = Notification(title='Suspicious content', url='/admin/reports', user_id=admin.id, author_id=current_user.id)
|
||||
db.session.add(notify)
|
||||
admin.unread_notifications += 1
|
||||
db.session.commit()
|
||||
|
|
|
@ -10,6 +10,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
setupLightDark();
|
||||
setupKeyboardShortcuts();
|
||||
setupTopicChooser();
|
||||
setupConversationChooser();
|
||||
});
|
||||
|
||||
|
||||
|
@ -508,6 +509,16 @@ function setupTopicChooser() {
|
|||
});
|
||||
}
|
||||
|
||||
function setupConversationChooser() {
|
||||
const changeSender = document.getElementById('changeSender');
|
||||
if(changeSender) {
|
||||
changeSender.addEventListener('change', function() {
|
||||
const user_id = changeSender.options[changeSender.selectedIndex].value;
|
||||
location.href = '/chat/' + user_id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
|
|
@ -1205,4 +1205,28 @@ fieldset legend {
|
|||
line-height: 32px;
|
||||
}
|
||||
|
||||
.conversation .message {
|
||||
width: 90%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 15px;
|
||||
clear: both;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.conversation .message {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
.conversation .message.from_other_party {
|
||||
float: right;
|
||||
}
|
||||
.conversation .message.from_me {
|
||||
float: left;
|
||||
}
|
||||
.conversation .message_created_at {
|
||||
float: right;
|
||||
}
|
||||
.conversation form .form-control-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=structure.css.map */
|
||||
|
|
|
@ -895,3 +895,27 @@ fieldset {
|
|||
}
|
||||
}
|
||||
|
||||
.conversation {
|
||||
.message {
|
||||
width: 90%;
|
||||
@include breakpoint(tablet) {
|
||||
width: 70%;
|
||||
}
|
||||
max-width: 100%;
|
||||
margin-bottom: 15px;
|
||||
clear: both;
|
||||
|
||||
&.from_other_party {
|
||||
float: right;
|
||||
}
|
||||
&.from_me {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
.message_created_at {
|
||||
float: right;
|
||||
}
|
||||
form .form-control-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -693,6 +693,24 @@ div.navbar {
|
|||
}
|
||||
}
|
||||
|
||||
.sender_list {
|
||||
border-right: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.message {
|
||||
border: solid 1px #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 8px 15px 0 15px;
|
||||
}
|
||||
.message.from_other_party {
|
||||
float: right;
|
||||
}
|
||||
.message.from_me {
|
||||
color: var(--bs-card-cap-color);
|
||||
background-color: var(--bs-card-cap-bg);
|
||||
}
|
||||
|
||||
/* high contrast */
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--bs-link-color: black;
|
||||
|
|
|
@ -319,6 +319,26 @@ div.navbar {
|
|||
}
|
||||
}
|
||||
|
||||
.sender_list {
|
||||
border-right: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.message {
|
||||
border: solid 1px #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 8px 15px 0 15px;
|
||||
|
||||
&.from_other_party {
|
||||
float: right;
|
||||
}
|
||||
&.from_me {
|
||||
color: var(--bs-card-cap-color);
|
||||
background-color: var(--bs-card-cap-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* high contrast */
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--bs-link-color: black;
|
||||
|
|
68
app/templates/chat/home.html
Normal file
68
app/templates/chat/home.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% 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="row">
|
||||
<div class="col main_pane">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ _('Chat') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ _('Chat') }}</h1>
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</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>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</select></h3>
|
||||
<div class="conversation">
|
||||
{% 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 }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -325,7 +325,7 @@ def report_profile(actor):
|
|||
already_notified = set()
|
||||
for admin in Site.admins():
|
||||
if admin.id not in already_notified:
|
||||
notify = Notification(title='Reported user', url=user.ap_id, user_id=admin.id, author_id=current_user.id)
|
||||
notify = Notification(title='Reported user', url='/admin/reports', user_id=admin.id, author_id=current_user.id)
|
||||
db.session.add(notify)
|
||||
admin.unread_notifications += 1
|
||||
user.reports += 1
|
||||
|
|
55
migrations/versions/fe1e3fbf5b9d_chat.py
Normal file
55
migrations/versions/fe1e3fbf5b9d_chat.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""chat
|
||||
|
||||
Revision ID: fe1e3fbf5b9d
|
||||
Revises: 75f5b458c2f9
|
||||
Create Date: 2024-02-17 09:53:47.915062
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fe1e3fbf5b9d'
|
||||
down_revision = '75f5b458c2f9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('chat_message',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('sender_id', sa.Integer(), nullable=True),
|
||||
sa.Column('recipient_id', sa.Integer(), nullable=True),
|
||||
sa.Column('body', sa.Text(), nullable=True),
|
||||
sa.Column('body_html', sa.Text(), nullable=True),
|
||||
sa.Column('reported', sa.Boolean(), nullable=True),
|
||||
sa.Column('read', sa.Boolean(), nullable=True),
|
||||
sa.Column('encrypted', sa.String(length=15), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['recipient_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('chat_message', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_chat_message_recipient_id'), ['recipient_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_chat_message_sender_id'), ['sender_id'], unique=False)
|
||||
|
||||
with op.batch_alter_table('site', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('auto_decline_referrers', sa.Text(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('site', schema=None) as batch_op:
|
||||
batch_op.drop_column('auto_decline_referrers')
|
||||
|
||||
with op.batch_alter_table('chat_message', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_chat_message_sender_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_chat_message_recipient_id'))
|
||||
|
||||
op.drop_table('chat_message')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in a new issue