FEATURE: Store server-side cooked messages (#238)

FEATURE: Store server-side cooked messages (#238)

diff --git a/app/controllers/chat_controller.rb b/app/controllers/chat_controller.rb
index 4f4f3c3..53507a6 100644
--- a/app/controllers/chat_controller.rb
+++ b/app/controllers/chat_controller.rb
@@ -165,7 +165,7 @@ class DiscourseChat::ChatController < DiscourseChat::ChatBaseController
       chatable: @chatable,
       messages: messages
     )
-    render_serialized(chat_view, ChatViewSerializer, root: :topic_chat_view)
+    render_serialized(chat_view, ChatViewSerializer, root: :chat_view)
   end
 
   def delete
@@ -229,7 +229,7 @@ class DiscourseChat::ChatController < DiscourseChat::ChatBaseController
       chatable: chatable,
       messages: messages
     )
-    render_serialized(chat_view, ChatViewSerializer, root: :topic_chat_view)
+    render_serialized(chat_view, ChatViewSerializer, root: :chat_view)
   end
 
   def set_user_chat_status
diff --git a/app/jobs/regular/process_chat_message.rb b/app/jobs/regular/process_chat_message.rb
new file mode 100644
index 0000000..14de828
--- /dev/null
+++ b/app/jobs/regular/process_chat_message.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Jobs
+  class ProcessChatMessage < ::Jobs::Base
+    def execute(args = {})
+      DistributedMutex.synchronize("process_chat_message_#{args[:chat_message_id]}", validity: 10.minutes) do
+        chat_message = ChatMessage.find_by(id: args[:chat_message_id])
+        processor = DiscourseChat::ChatMessageProcessor.new(chat_message)
+        processor.run!
+        if processor.dirty?
+          chat_message.update(
+            cooked: processor.html,
+            cooked_version: ChatMessage::BAKED_VERSION
+          )
+          ChatPublisher.publish_processed!(chat_message)
+        end
+      end
+    end
+  end
+end
diff --git a/app/models/chat_message.rb b/app/models/chat_message.rb
index 3f31e61..49bbb33 100644
--- a/app/models/chat_message.rb
+++ b/app/models/chat_message.rb
@@ -3,6 +3,9 @@
 class ChatMessage < ActiveRecord::Base
   include Trashable
   self.ignored_columns = ["post_id"]
+  attribute :has_oneboxes, default: false
+
+  BAKED_VERSION = 1
 
   belongs_to :chat_channel
   belongs_to :user
@@ -16,4 +19,84 @@ class ChatMessage < ActiveRecord::Base
     raise NotImplementedError
     #ReviewableFlaggedChat.pending.find_by(target: self)
   end
+
+  def excerpt
+    PrettyText.excerpt(cooked, 50, {})
+  end
+
+  def self.uncooked
+    where('cooked_version <> ? or cooked_version IS NULL', BAKED_VERSION)
+  end
+
+  def self.cook(message, opts = {})
+    cooked = PrettyText.cook(message, features: COOK_FEATURES)
+    result = Oneboxer.apply(cooked) do |url|
+      if opts[:invalidate_oneboxes]
+        Oneboxer.invalidate(url)
+        InlineOneboxer.invalidate(url)
+      end
+      onebox = Oneboxer.cached_onebox(url)
+      onebox
+    end
+
+    cooked = result.to_html if result.changed?
+    cooked
+  end
+
+  def cook
+    self.cooked = self.class.cook(self.message)
+    self.cooked_version = BAKED_VERSION
+  end
+
+  def rebake!(invalidate_oneboxes: false, priority: nil)
+    new_cooked = self.class.cook(message, invalidate_oneboxes: invalidate_oneboxes)
+    update_columns(
+      cooked: new_cooked,
+      cooked_version: BAKED_VERSION
+    )
+    args = {
+      chat_message_id: self.id,
+    }
+    args[:queue] = priority.to_s if priority && priority != :normal
+
+    Jobs.enqueue(:process_chat_message, args)
+  end
+
+  COOK_FEATURES = {
+    anchor: true,
+    "auto-link": true,
+    bbcode: true,
+    "bbcode-block": true,
+    "bbcode-inline": true,
+    "bold-italics": true,
+    "category-hashtag": true,
+    censored: true,
+    checklist: false,
+    code: true,
+    "custom-typographer-replacements": false,
+    "d-wrap": false,
+    details: false,
+    "discourse-local-dates": true,
+    emoji: true,
+    emojiShortcuts: true,
+    html: false,
+    "html-img": true,
+    "inject-line-number": true,
+    inlineEmoji: true,
+    linkify: true,
+    mentions: true,
+    newline: true,
+    onebox: true,
+    paragraph: false,
+    policy: false,
+    poll: false,
+    quote: true,
+    quotes: true,
+    "resize-controls": false,
+    table: true,
+    "text-post-process": true,
+    unicodeUsernames: false,
+    "upload-protocol": true,
+    "watched-words": true,
+  }
 end
diff --git a/app/serializers/chat_base_message_serializer.rb b/app/serializers/chat_base_message_serializer.rb
index f071423..b2c9a8c 100644
--- a/app/serializers/chat_base_message_serializer.rb
+++ b/app/serializers/chat_base_message_serializer.rb
@@ -3,8 +3,10 @@
 class ChatBaseMessageSerializer < ApplicationSerializer
   attributes :id,
     :message,
+    :cooked,
     :action_code,
     :created_at,
+    :excerpt,
     :in_reply_to_id,
     :deleted_at,
     :deleted_by_id,
diff --git a/app/services/chat_publisher.rb b/app/services/chat_publisher.rb
index 8a98a8f..faf40aa 100644
--- a/app/services/chat_publisher.rb
+++ b/app/services/chat_publisher.rb
@@ -2,7 +2,7 @@
 
 module ChatPublisher
   def self.publish_new!(chat_channel, msg, staged_id)
-    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :topic_chat_message }).as_json
+    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :chat_message }).as_json
     content[:typ] = :sent
     content[:stagedId] = staged_id
     permissions = permissions(chat_channel)
@@ -10,8 +10,20 @@ module ChatPublisher
     MessageBus.publish("/chat/#{chat_channel.id}/new-messages", { message_id: msg.id, user_id: msg.user_id }, permissions)
   end
 
+  def self.publish_processed!(chat_message)
+    chat_channel = chat_message.chat_channel
+    content = {
+      typ: :processed,
+      chat_message: {
+        id: chat_message.id,
+        cooked: chat_message.cooked
+      }
+    }
+    MessageBus.publish("/chat/#{chat_channel.id}", content.as_json, permissions(chat_channel))
+  end
+
   def self.publish_edit!(chat_channel, msg)
-    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :topic_chat_message }).as_json
+    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :chat_message }).as_json
     content[:typ] = :edit
     MessageBus.publish("/chat/#{chat_channel.id}", content.as_json, permissions(chat_channel))
   end
@@ -29,7 +41,7 @@ module ChatPublisher
   end
 
   def self.publish_restore!(chat_channel, msg)
-    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :topic_chat_message }).as_json
+    content = ChatBaseMessageSerializer.new(msg, { scope: anonymous_guardian, root: :chat_message }).as_json
     content[:typ] = :restore
     MessageBus.publish("/chat/#{chat_channel.id}", content.as_json, permissions(chat_channel))
   end
diff --git a/assets/javascripts/discourse/components/chat-live-pane.js b/assets/javascripts/discourse/components/chat-live-pane.js
index 72a7d96..0b28d42 100644
--- a/assets/javascripts/discourse/components/chat-live-pane.js
+++ b/assets/javascripts/discourse/components/chat-live-pane.js
@@ -12,7 +12,6 @@ import { isTesting } from "discourse-common/config/environment";
 import { popupAjaxError } from "discourse/lib/ajax-error";
 import { cancel, later, next, schedule } from "@ember/runloop";
 import { inject as service } from "@ember/service";
-import { loadOneboxes } from "discourse/lib/load-oneboxes";
 import { Promise } from "rsvp";
 import { resetIdle } from "discourse/lib/desktop-notifications";
 import { resolveAllShortUrls } from "pretty-text/upload-short-url";
@@ -138,7 +137,7 @@ export default Component.extend({
           if (this._selfDeleted()) {
             return;
           }
-          this.setMessageProps(data.topic_chat_view);
+          this.setMessageProps(data.chat_view);
           this.decorateMessages();
           this.onScroll();
         })
@@ -173,7 +172,7 @@ export default Component.extend({
           return;
         }
         const newMessages = this._prepareMessages(

[... diff too long, it was truncated ...]

GitHub sha: 3ae72b814b657d82cfdcdb72d40c3547777c25e7

This commit appears in #238 which was approved by eviltrout. It was merged by markvanlan.