Module: Clacky::Channel::Adapters::Wecom::MediaDownloader

Defined in:
lib/clacky/server/channel/adapters/wecom/media_downloader.rb

Overview

Downloads and decrypts media files from WeCom message URLs.

WeCom long-connection bot messages include a per-resource aeskey. Encryption: AES-256-CBC, key=base64_decode(aeskey), iv=key, no PKCS7 padding. However some files are sent unencrypted — detect via magic bytes before decrypting.

Constant Summary collapse

HTTP_TIMEOUT =
30
MAGIC_SIGNATURES =

Check if data looks like a plain (unencrypted) file via magic bytes.

[
  "\xFF\xD8\xFF",         # JPEG
  "\x89PNG\r\n\x1a\n",   # PNG
  "GIF8",                  # GIF
  "%PDF",                  # PDF
  "PK\x03\x04",           # ZIP (docx/xlsx)
  "\xD0\xCF\x11\xE0",    # OLE2 (doc/xls)
  "RIFF",                  # WAV/WebP
].map { |s| s.b }.freeze

Class Method Summary collapse

Class Method Details

.detect_mime(data) ⇒ String

Detect MIME type from magic bytes

Parameters:

  • data (String)

    Binary data

Returns:

  • (String)

    MIME type



99
100
101
102
103
104
105
106
107
108
# File 'lib/clacky/server/channel/adapters/wecom/media_downloader.rb', line 99

def self.detect_mime(data)
  return "application/octet-stream" if data.nil? || data.empty?
  d = data.b
  return "image/jpeg"      if d.start_with?("\xFF\xD8\xFF".b)
  return "image/png"       if d.start_with?("\x89PNG\r\n\x1a\n".b)
  return "image/gif"       if d.start_with?("GIF8".b)
  return "image/webp"      if d.start_with?("RIFF".b) && d.byteslice(8, 4) == "WEBP".b
  return "image/bmp"       if d.start_with?("BM".b)
  "image/jpeg"  # fallback for unknown image formats
end

.download(url, aeskey) ⇒ Hash

Download and decrypt a WeCom media resource.

Parameters:

  • url (String)

    Signed download URL from the message

  • aeskey (String)

    Per-resource AES key string

Returns:

  • (Hash)

    { body: String (binary), content_type: String }



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/clacky/server/channel/adapters/wecom/media_downloader.rb', line 23

def self.download(url, aeskey)
  response = fetch(url)
  body = response.body.dup.force_encoding("BINARY")

  if aeskey && !aeskey.empty? && !looks_plain?(body)
    body = decrypt(body, aeskey)
  end

  content_type = detect_mime(body)
  filename = extract_filename(response.headers["content-disposition"].to_s)
  { body: body, content_type: content_type, filename: filename }
end