FIX: automatically bust cache for share ai assets (#942)

* FIX: automatically bust cache for share ai assets

CDNs can be configured to strip query params in Discourse
hosting. This is generally safe, but in this case we had
no way of busting the cache using the path.

New design properly caches and properly breaks busts the
cache if asset changes so we don't need to worry about versions

* one day I will set up conditional lint on save :)
This commit is contained in:
Sam 2024-11-22 11:23:15 +11:00 committed by GitHub
parent 2c78961bed
commit 86cf4ccba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 9 deletions

View File

@ -7,7 +7,8 @@ module DiscourseAi
requires_login only: %i[create update destroy] requires_login only: %i[create update destroy]
before_action :require_site_settings! before_action :require_site_settings!
skip_before_action :preload_json, :check_xhr, only: %i[show] skip_before_action :preload_json, :check_xhr, only: %i[show asset]
skip_before_action :verify_authenticity_token, only: ["asset"]
def create def create
ensure_allowed_create! ensure_allowed_create!
@ -50,6 +51,30 @@ module DiscourseAi
end end
end end
def asset
no_cookies
name = params[:name]
path, content_type =
if name == "share"
%w[share.css text/css]
elsif name == "highlight"
%w[highlight.min.js application/javascript]
else
raise Discourse::NotFound
end
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
# note, path contains a ":version" which automatically busts the cache
# based on file content, so this is safe
response.headers["Last-Modified"] = 10.years.ago.httpdate
response.headers["Content-Length"] = content.bytesize.to_s
immutable_for 1.year
render plain: content, disposition: :nil, content_type: content_type
end
def preview def preview
ensure_allowed_preview! ensure_allowed_preview!
data = SharedAiConversation.build_conversation_data(@topic, include_usernames: true) data = SharedAiConversation.build_conversation_data(@topic, include_usernames: true)

View File

@ -2,13 +2,29 @@
module DiscourseAi module DiscourseAi
module AiBot module AiBot
module SharedAiConversationsHelper module SharedAiConversationsHelper
# bump up version when assets change # keeping it here for caching
# long term we may want to change this cause it is hard to remember def self.share_asset_url(asset_name)
# to bump versions, but for now this does the job if !%w[share.css highlight.js].include?(asset_name)
VERSION = "1" raise StandardError, "unknown asset type #{asset_name}"
end
def share_asset_url(short_path) @urls ||= {}
::UrlHelper.absolute("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}") url = @urls[asset_name]
return url if url
path = asset_name
path = "highlight.min.js" if asset_name == "highlight.js"
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
sha1 = Digest::SHA1.hexdigest(content)
url = "/discourse-ai/ai-bot/shared-ai-conversations/asset/#{sha1}/#{asset_name}"
@urls[asset_name] = GlobalPath.cdn_path(url)
end
def share_asset_url(asset_name)
DiscourseAi::AiBot::SharedAiConversationsHelper.share_asset_url(asset_name)
end end
end end
end end

View File

@ -10,7 +10,7 @@
<meta name="twitter:title" content="<%= I18n.t("discourse_ai.share_ai.title", title: @shared_conversation.title, site_name: SiteSetting.title) %>"> <meta name="twitter:title" content="<%= I18n.t("discourse_ai.share_ai.title", title: @shared_conversation.title, site_name: SiteSetting.title) %>">
<meta name="twitter:description" content="<%= @shared_conversation.formatted_excerpt %>"> <meta name="twitter:description" content="<%= @shared_conversation.formatted_excerpt %>">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="<%= ::UrlHelper.absolute("/plugins/discourse-ai/ai-share/share.css?v=1") %>"> <link rel="stylesheet" href="<%= share_asset_url("share.css") %>">
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@ -54,7 +54,7 @@
</article> </article>
<% end %> <% end %>
</section> </section>
<script src="<%= share_asset_url("highlight.min.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script> <script src="<%= share_asset_url("highlight.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script>
<script nonce="<%= csp_nonce_placeholder %>"> <script nonce="<%= csp_nonce_placeholder %>">
document.querySelectorAll('pre code').forEach((el) => { document.querySelectorAll('pre code').forEach((el) => {
hljs.highlightElement(el); hljs.highlightElement(el);

View File

@ -30,6 +30,7 @@ DiscourseAi::Engine.routes.draw do
post "/" => "shared_ai_conversations#create" post "/" => "shared_ai_conversations#create"
delete "/:share_key" => "shared_ai_conversations#destroy" delete "/:share_key" => "shared_ai_conversations#destroy"
get "/:share_key" => "shared_ai_conversations#show" get "/:share_key" => "shared_ai_conversations#show"
get "/asset/:version/:name" => "shared_ai_conversations#asset"
get "/preview/:topic_id" => "shared_ai_conversations#preview" get "/preview/:topic_id" => "shared_ai_conversations#preview"
end end

View File

@ -43,6 +43,10 @@ register_asset "stylesheets/modules/ai-bot/common/ai-artifact.scss"
module ::DiscourseAi module ::DiscourseAi
PLUGIN_NAME = "discourse-ai" PLUGIN_NAME = "discourse-ai"
def self.public_asset_path(name)
File.expand_path(File.join(__dir__, "public", name))
end
end end
Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::DiscourseAi) Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::DiscourseAi)

View File

@ -294,6 +294,30 @@ RSpec.describe DiscourseAi::AiBot::SharedAiConversationsController do
end end
end end
describe "GET asset" do
let(:helper) { Class.new { extend DiscourseAi::AiBot::SharedAiConversationsHelper } }
it "renders highlight js correctly" do
get helper.share_asset_url("highlight.js")
expect(response).to be_successful
expect(response.headers["Content-Type"]).to eq("application/javascript; charset=utf-8")
js = File.read(DiscourseAi.public_asset_path("ai-share/highlight.min.js"))
expect(response.body).to eq(js)
end
it "renders css correctly" do
get helper.share_asset_url("share.css")
expect(response).to be_successful
expect(response.headers["Content-Type"]).to eq("text/css; charset=utf-8")
css = File.read(DiscourseAi.public_asset_path("ai-share/share.css"))
expect(response.body).to eq(css)
end
end
describe "GET preview" do describe "GET preview" do
it "denies preview from logged out users" do it "denies preview from logged out users" do
get "#{path}/preview/#{user_pm_share.id}.json" get "#{path}/preview/#{user_pm_share.id}.json"