mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 17:06:31 -06:00
FEATURE: Export chat messages to CSV file (#22113)
To export chat messages, go to `/admin/plugins/chat` and click the Create export button in the _Export chat messages_ section. You'll receive a direct message when the export is finished. Currently, this exports all messages from the last 6 months, but not more than 10000 messages. This exports all chat messages, including messages from private channels and users' direct conversations. This also exports messages that were deleted.
This commit is contained in:
parent
720c0c6e4d
commit
3ea31f443c
@ -411,6 +411,10 @@ $mobile-breakpoint: 700px;
|
||||
.admin-container {
|
||||
margin-top: 10px;
|
||||
|
||||
.admin-section {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.username {
|
||||
input {
|
||||
min-width: 15em;
|
||||
|
15
plugins/chat/app/controllers/chat/admin/export_controller.rb
Normal file
15
plugins/chat/app/controllers/chat/admin/export_controller.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module Admin
|
||||
class ExportController < ::Admin::AdminController
|
||||
requires_plugin Chat::PLUGIN_NAME
|
||||
|
||||
def export_messages
|
||||
entity = "chat_message"
|
||||
Jobs.enqueue(:export_csv_file, entity: entity, user_id: current_user.id)
|
||||
StaffActionLogger.new(current_user).log_entity_export(entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
<section class="admin-section">
|
||||
<h3>{{i18n "chat.admin.export_messages.title"}}</h3>
|
||||
<p>{{i18n "chat.admin.export_messages.description"}}</p>
|
||||
<DButton
|
||||
@label="chat.admin.export_messages.create_export"
|
||||
@title="chat.admin.export_messages.create_export"
|
||||
@class="btn-primary"
|
||||
@action={{action this.exportMessages}}
|
||||
/>
|
||||
</section>
|
@ -0,0 +1,22 @@
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class ExportMessages extends Component {
|
||||
@service chatAdminApi;
|
||||
@service dialog;
|
||||
|
||||
@action
|
||||
async exportMessages() {
|
||||
try {
|
||||
await this.chatAdminApi.exportMessages();
|
||||
this.dialog.alert(
|
||||
I18n.t("chat.admin.export_messages.export_has_started")
|
||||
);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import Service from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default class ChatAdminApi extends Service {
|
||||
async exportMessages() {
|
||||
await this.#post(`/export/messages`);
|
||||
}
|
||||
|
||||
get #basePath() {
|
||||
return "/chat/admin";
|
||||
}
|
||||
|
||||
#post(endpoint, data = {}) {
|
||||
return ajax(`${this.#basePath}${endpoint}`, {
|
||||
type: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
<Chat::Admin::ExportMessages />
|
||||
|
||||
{{#if this.selectedWebhook}}
|
||||
<DButton
|
||||
@class="incoming-chat-webhooks-back"
|
||||
|
@ -120,7 +120,7 @@ en:
|
||||
description: "Select an option below to summarize the conversation sent during the desired timeframe."
|
||||
summarize: "Summarize"
|
||||
since:
|
||||
one: "Last hour"
|
||||
one: "Last hour"
|
||||
other: "Last %{count} hours"
|
||||
mention_warning:
|
||||
dismiss: "dismiss"
|
||||
@ -451,6 +451,11 @@ en:
|
||||
|
||||
admin:
|
||||
title: "Chat"
|
||||
export_messages:
|
||||
title: "Export chat messages"
|
||||
description: "Export is currently limited to 10000 most recent messages in the last 6 months."
|
||||
create_export: "Create export"
|
||||
export_has_started: "The export has started. You'll receive a PM when it's ready."
|
||||
|
||||
direct_messages:
|
||||
title: "Personal chat"
|
||||
|
@ -41,6 +41,10 @@ Chat::Engine.routes.draw do
|
||||
get "/channels/:channel_id/summarize" => "summaries#get_summary"
|
||||
end
|
||||
|
||||
namespace :admin, defaults: { format: :json, constraints: StaffConstraint.new } do
|
||||
post "export/messages" => "export#export_messages"
|
||||
end
|
||||
|
||||
# direct_messages_controller routes
|
||||
get "/direct_messages" => "direct_messages#index"
|
||||
post "/direct_messages/create" => "direct_messages#create"
|
||||
|
55
plugins/chat/lib/chat/messages_exporter.rb
Normal file
55
plugins/chat/lib/chat/messages_exporter.rb
Normal file
@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module MessagesExporter
|
||||
LIMIT = 10_000
|
||||
|
||||
def chat_message_export
|
||||
Chat::Message
|
||||
.unscoped
|
||||
.where(created_at: 6.months.ago..Time.current)
|
||||
.joins(:chat_channel)
|
||||
.joins(:user)
|
||||
.joins("INNER JOIN users last_editors ON chat_messages.last_editor_id = last_editors.id")
|
||||
.order(:created_at)
|
||||
.limit(LIMIT)
|
||||
.pluck(
|
||||
"chat_messages.id",
|
||||
"chat_channels.id",
|
||||
"chat_channels.name",
|
||||
"users.id",
|
||||
"users.username",
|
||||
"chat_messages.message",
|
||||
"chat_messages.cooked",
|
||||
"chat_messages.created_at",
|
||||
"chat_messages.updated_at",
|
||||
"chat_messages.deleted_at",
|
||||
"chat_messages.in_reply_to_id",
|
||||
"last_editors.id",
|
||||
"last_editors.username",
|
||||
)
|
||||
end
|
||||
|
||||
def get_header(entity)
|
||||
if entity === "chat_message"
|
||||
%w[
|
||||
id
|
||||
chat_channel_id
|
||||
chat_channel_name
|
||||
user_id
|
||||
username
|
||||
message
|
||||
cooked
|
||||
created_at
|
||||
updated_at
|
||||
deleted_at
|
||||
in_reply_to_id
|
||||
last_editor_id
|
||||
last_editor_username
|
||||
]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -63,6 +63,7 @@ after_initialize do
|
||||
User.prepend Chat::UserExtension
|
||||
Jobs::UserEmail.prepend Chat::UserEmailExtension
|
||||
Plugin::Instance.prepend Chat::PluginInstanceExtension
|
||||
Jobs::ExportCsvFile.class_eval { prepend Chat::MessagesExporter }
|
||||
end
|
||||
|
||||
if Oneboxer.respond_to?(:register_local_handler)
|
||||
|
52
plugins/chat/spec/lib/chat/messages_exporter_spec.rb
Normal file
52
plugins/chat/spec/lib/chat/messages_exporter_spec.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Chat::MessagesExporter do
|
||||
fab!(:public_channel) { Fabricate(:chat_channel) }
|
||||
fab!(:public_channel_message_1) { Fabricate(:chat_message, chat_channel: public_channel) }
|
||||
fab!(:public_channel_message_2) { Fabricate(:chat_message, chat_channel: public_channel) }
|
||||
# this message is deleted in the before block:
|
||||
fab!(:deleted_message) { Fabricate(:chat_message, chat_channel: public_channel) }
|
||||
|
||||
fab!(:private_channel) { Fabricate(:private_category_channel, group: Fabricate(:group)) }
|
||||
fab!(:private_channel_message_1) { Fabricate(:chat_message, chat_channel: private_channel) }
|
||||
fab!(:private_channel_message_2) { Fabricate(:chat_message, chat_channel: private_channel) }
|
||||
|
||||
fab!(:user_1) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||
fab!(:direct_message_1) { Fabricate(:chat_message, chat_channel: private_channel, user: user_1) }
|
||||
fab!(:direct_message_2) { Fabricate(:chat_message, chat_channel: private_channel, user: user_2) }
|
||||
|
||||
before { deleted_message.trash! }
|
||||
|
||||
it "exports messages" do
|
||||
exporter = Class.new.extend(Chat::MessagesExporter)
|
||||
|
||||
result = exporter.chat_message_export.to_a
|
||||
|
||||
expect(result.length).to be(7)
|
||||
assert_exported_message(result[0], public_channel_message_1)
|
||||
assert_exported_message(result[1], public_channel_message_2)
|
||||
assert_exported_message(result[2], deleted_message)
|
||||
assert_exported_message(result[3], private_channel_message_1)
|
||||
assert_exported_message(result[4], private_channel_message_2)
|
||||
assert_exported_message(result[5], direct_message_1)
|
||||
assert_exported_message(result[6], direct_message_2)
|
||||
end
|
||||
|
||||
def assert_exported_message(data_row, message)
|
||||
expect(data_row[0]).to eq(message.id)
|
||||
expect(data_row[1]).to eq(message.chat_channel.id)
|
||||
expect(data_row[2]).to eq(message.chat_channel.name)
|
||||
expect(data_row[3]).to eq(message.user.id)
|
||||
expect(data_row[4]).to eq(message.user.username)
|
||||
expect(data_row[5]).to eq(message.message)
|
||||
expect(data_row[6]).to eq(message.cooked)
|
||||
expect(data_row[7]).to eq_time(message.created_at)
|
||||
expect(data_row[8]).to eq_time(message.updated_at)
|
||||
expect(data_row[9]).to eq_time(message.deleted_at)
|
||||
expect(data_row[10]).to eq(message.in_reply_to_id)
|
||||
expect(data_row[11]).to eq(message.last_editor.id)
|
||||
expect(data_row[12]).to eq(message.last_editor.username)
|
||||
end
|
||||
end
|
40
plugins/chat/spec/requests/admin/export_controller_spec.rb
Normal file
40
plugins/chat/spec/requests/admin/export_controller_spec.rb
Normal file
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Chat::ChatController do
|
||||
describe "#export_messages" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:moderator) { Fabricate(:moderator) }
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
|
||||
it "enqueues the export job and logs into staff actions" do
|
||||
sign_in(admin)
|
||||
|
||||
post "/chat/admin/export/messages"
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
|
||||
expect(Jobs::ExportCsvFile.jobs.size).to eq(1)
|
||||
job_data = Jobs::ExportCsvFile.jobs.first["args"].first
|
||||
expect(job_data["entity"]).to eq("chat_message")
|
||||
expect(job_data["user_id"]).to eq(admin.id)
|
||||
|
||||
staff_log_entry = UserHistory.last
|
||||
expect(staff_log_entry.acting_user_id).to eq(admin.id)
|
||||
expect(staff_log_entry.subject).to eq("chat_message")
|
||||
end
|
||||
|
||||
it "regular users don't have access" do
|
||||
sign_in(user)
|
||||
post "/chat/admin/export/messages"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "moderators don't have access" do
|
||||
sign_in(moderator)
|
||||
post "/chat/admin/export/messages"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user