Class: Assistant::BlogMediaToolBuilder

Inherits:
Object
  • Object
show all
Defined in:
app/services/assistant/blog_media_tool_builder.rb

Defined Under Namespace

Classes: EmbedOpGuardError

Constant Summary collapse

CRM_POST_URL =
BlogToolBuilder::CRM_POST_URL

Class Method Summary collapse

Class Method Details

.find_embed_figure(html, uuid) ⇒ Object

Locate the embed wrapper by its data-embedded-asset-uuid in the post
solution. Most embeds are , but video embeds are sometimes
rendered as (see
BlogContentValidator#detect_dropped_embeds for the full list). Match
any element carrying the attribute to stay aligned with the validator.
Returns [node, fragment] or [nil, nil].

NOTE: do NOT interpolate uuid into a CSS selector string. embed UUIDs
come straight from tool input, so quotes/CSS metacharacters (e.g. a
value like "]/script>) can turn a stale or malicious value into a
selector parse error — or worse, match the wrong node before we
mutate the fragment. Iterate the candidate nodes and compare the
attribute by string equality instead.



535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'app/services/assistant/blog_media_tool_builder.rb', line 535

def find_embed_figure(html, uuid)
  return [nil, nil] if html.nil? || html.to_s.empty? || uuid.to_s.empty?

  fragment = if defined?(Nokogiri::HTML5)
               Nokogiri::HTML5.fragment(html.to_s)
             else
               Nokogiri::HTML.fragment(html.to_s)
             end

  uuid_str = uuid.to_s
  node = fragment.css('[data-embedded-asset-uuid]').find do |el|
    el['data-embedded-asset-uuid'] == uuid_str
  end
  [node, fragment]
end

.save_post_with_embed_op!(post, new_html, change_note:, audit_context:) ⇒ Object

Raises:



555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'app/services/assistant/blog_media_tool_builder.rb', line 555

def save_post_with_embed_op!(post, new_html, change_note:, audit_context:)
  # These atomic embed ops (remove_embed / replace_embed / move_embed)
  # write post.solution directly, so without this guard a hallucinated
  # <figure data-embedded-asset-uuid="..."> from replace_embed could
  # land in the database without going through the same body / link /
  # asset-reference checks update_blog_post and edit_blog_post rely on.
  # We pass skip_embed_check: true because the user's intent to drop or
  # rearrange the targeted UUID is explicit (that's the whole point of
  # these tools), but every other content rule must still pass.
  guard_err = Assistant::BlogToolBuilder.guard_content(
    body_args: {
      old_html: post.solution.to_s,
      new_html: new_html.to_s,
      post_id: post.id,
      skip_embed_check: true
    }
  )
  raise EmbedOpGuardError, guard_err if guard_err

  author = Employee.find_by(id: audit_context[:user_id])
  post.solution = new_html
  post.auto_update_revised_at = true if post.respond_to?(:auto_update_revised_at=)
  post.updater_id = author&.id
  post.revision_change_notes = change_note
  post.save!
  post.reload
  Assistant::BlogToolBuilder.stabilize_post_solution_after_save!(post)
  Assistant::BlogContentValidator.upsert_link_graph(post)
  post
end

.serialize_fragment(fragment) ⇒ Object



551
552
553
# File 'app/services/assistant/blog_media_tool_builder.rb', line 551

def serialize_fragment(fragment)
  fragment.to_html
end

.tools(audit_context: {}) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
# File 'app/services/assistant/blog_media_tool_builder.rb', line 22

def tools(audit_context: {})
  [
    build_insert_image_tool,
    build_insert_video_tool,
    build_insert_faqs_tool,
    build_insert_product_tool,
    build_remove_embed_tool(audit_context),
    build_replace_embed_tool(audit_context),
    build_move_embed_tool(audit_context),
    build_refresh_blog_oembeds_tool(audit_context),
  ]
end