diff --git a/API.md b/API.md index c6a2216d..fcdca679 100644 --- a/API.md +++ b/API.md @@ -68,12 +68,16 @@ Response: You can create a user with optional proxy and S3 storage configuration. All fields are optional and backward compatible. If you do not provide these fields, the user will be created with default settings. +The optional `skipMedia` (boolean) field controls the per-instance media download setting. When `true`, the instance does not automatically download media from incoming messages. It defaults to `false` (media is downloaded) and can be changed later via [`/session/skipmedia`](#user-content-set-media-download-setting). + ### Example Payload ```json { "name": "test_user", "token": "user_token", + "history": 0, + "skipMedia": false, "proxyConfig": { "enabled": true, "proxyURL": "socks5://user:pass@host:port" @@ -485,6 +489,65 @@ Response: --- +## Get Media Download Setting + +Retrieves the per-instance `skip_media` setting. When `skip_media` is `true`, the server does not automatically download media (images, audio, documents, etc.) from incoming messages for this instance. It defaults to `false` (media is downloaded), preserving the existing behavior. + +This per-instance setting is independent from the global `-skipmedia` flag. Media is only downloaded when both the global flag is unset **and** the instance `skip_media` is `false`. + +Endpoint: _/session/skipmedia_ + +Method: **GET** + +``` +curl -s -X GET -H 'Token: 1234ABCD' http://localhost:8080/session/skipmedia +``` + +Response: + +```json +{ + "code": 200, + "data": { + "skip_media": false + }, + "success": true +} +``` + +--- + +## Set Media Download Setting + +Updates the per-instance `skip_media` setting. Set `skip_media` to `true` to disable automatic media download for incoming messages on this instance, or `false` (the default) to enable it. + +Endpoint: _/session/skipmedia_ + +Method: **POST** + +Parameters: + +* `skip_media` (boolean, required): `true` to skip downloading media, `false` to download media automatically (default). + +``` +curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"skip_media":true}' http://localhost:8080/session/skipmedia +``` + +Response: + +```json +{ + "code": 200, + "data": { + "Details": "Skip media configured successfully", + "skip_media": true + }, + "success": true +} +``` + +--- + ## User The following _user_ endpoints are used to gather information about Whatsapp users. diff --git a/README.md b/README.md index 9b1f90a4..bac5c522 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,17 @@ you can use to alter behaviour * -logtype : format for logs, either console (default) or json * -color : enable colored output for console logs * -osname : Connection OS Name in Whatsapp -* -skipmedia : Skip downloading media from messages +* -skipmedia : Skip downloading media from messages (global, applies to all instances) * -wadebug : enable whatsmeow debug, either INFO or DEBUG levels are suported * -sslcertificate : SSL Certificate File * -sslprivatekey : SSL Private Key File +> Media download can also be controlled **per instance** via the `/session/skipmedia` endpoint +> (see [API.md](./API.md)). This setting defaults to `skip_media = false`, preserving the existing +> behavior (media is downloaded). Media is downloaded only when the global `-skipmedia` flag is +> unset **and** the instance `skip_media` is `false`. + Example: To have colored logs: diff --git a/handlers.go b/handlers.go index ee3c5d3e..c3a4ec12 100644 --- a/handlers.go +++ b/handlers.go @@ -161,16 +161,16 @@ func (s *server) authalice(next http.Handler) http.Handler { if !found { log.Info().Msg("Looking for user information in DB") // Checks DB from matching user and store user values in context - rows, err := s.db.Query("SELECT id,name,webhook,jid,events,proxy_url,qrcode,history,hmac_key IS NOT NULL AND length(hmac_key) > 0,CASE WHEN s3_enabled THEN 'true' ELSE 'false' END,COALESCE(media_delivery, 'base64') FROM users WHERE token=$1 LIMIT 1", token) + rows, err := s.db.Query("SELECT id,name,webhook,jid,events,proxy_url,qrcode,history,hmac_key IS NOT NULL AND length(hmac_key) > 0,CASE WHEN s3_enabled THEN 'true' ELSE 'false' END,COALESCE(media_delivery, 'base64'),CASE WHEN COALESCE(skip_media, false) THEN 'true' ELSE 'false' END FROM users WHERE token=$1 LIMIT 1", token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return } defer rows.Close() var history sql.NullInt64 - var s3Enabled, mediaDelivery string + var s3Enabled, mediaDelivery, skipMedia string for rows.Next() { - err = rows.Scan(&txtid, &name, &webhook, &jid, &events, &proxy_url, &qrcode, &history, &hasHmac, &s3Enabled, &mediaDelivery) + err = rows.Scan(&txtid, &name, &webhook, &jid, &events, &proxy_url, &qrcode, &history, &hasHmac, &s3Enabled, &mediaDelivery, &skipMedia) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -196,6 +196,7 @@ func (s *server) authalice(next http.Handler) http.Handler { "HasHmac": strconv.FormatBool(hasHmac), "S3Enabled": s3Enabled, "MediaDelivery": mediaDelivery, + "SkipMedia": skipMedia, }} userinfocache.Set(token, v, cache.NoExpiration) @@ -827,6 +828,7 @@ func (s *server) GetStatus() http.HandlerFunc { "proxy_url": userInfo.Get("Proxy"), "qrcode": userInfo.Get("Qrcode"), "history": userInfo.Get("History"), + "skip_media": userInfo.Get("SkipMedia") == "true", "proxy_config": proxyConfig, "s3_config": s3Config, "hmac_configured": hmacConfigured, @@ -5374,6 +5376,7 @@ func (s *server) AddUser() http.HandlerFunc { S3Config *S3Config `json:"s3Config,omitempty"` HmacKey string `json:"hmacKey,omitempty"` History int `json:"history,omitempty"` + SkipMedia bool `json:"skipMedia,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&user); err != nil { @@ -5480,9 +5483,9 @@ func (s *server) AddUser() http.HandlerFunc { // Insert user with all proxy, S3 and HMAC fields if _, err = s.db.Exec( - "INSERT INTO users (id, name, token, webhook, expiration, events, jid, qrcode, proxy_url, s3_enabled, s3_endpoint, s3_region, s3_bucket, s3_access_key, s3_secret_key, s3_path_style, s3_public_url, media_delivery, s3_retention_days, hmac_key, history) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)", + "INSERT INTO users (id, name, token, webhook, expiration, events, jid, qrcode, proxy_url, s3_enabled, s3_endpoint, s3_region, s3_bucket, s3_access_key, s3_secret_key, s3_path_style, s3_public_url, media_delivery, s3_retention_days, hmac_key, history, skip_media) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)", id, user.Name, user.Token, user.Webhook, user.Expiration, user.Events, "", "", user.ProxyConfig.ProxyURL, - user.S3Config.Enabled, user.S3Config.Endpoint, user.S3Config.Region, user.S3Config.Bucket, user.S3Config.AccessKey, user.S3Config.SecretKey, user.S3Config.PathStyle, user.S3Config.PublicURL, user.S3Config.MediaDelivery, user.S3Config.RetentionDays, encryptedHmacKey, user.History, + user.S3Config.Enabled, user.S3Config.Endpoint, user.S3Config.Region, user.S3Config.Bucket, user.S3Config.AccessKey, user.S3Config.SecretKey, user.S3Config.PathStyle, user.S3Config.PublicURL, user.S3Config.MediaDelivery, user.S3Config.RetentionDays, encryptedHmacKey, user.History, user.SkipMedia, ); err != nil { log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("admin DB error") s.respondWithJSON(w, http.StatusInternalServerError, map[string]interface{}{ @@ -5536,6 +5539,7 @@ func (s *server) AddUser() http.HandlerFunc { "proxy_config": proxyConfig, "s3_config": s3Config, "hmac_key": user.HmacKey != "", + "skip_media": user.SkipMedia, } s.respondWithJSON(w, http.StatusCreated, map[string]interface{}{ "code": http.StatusCreated, @@ -6053,6 +6057,83 @@ func (s *server) SetHistory() http.HandlerFunc { } } +// GetSkipMedia returns the per-instance skip_media setting. When enabled the +// server does not automatically download media from incoming messages for this +// instance. It defaults to false (media is downloaded). +func (s *server) GetSkipMedia() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + txtid := r.Context().Value("userinfo").(Values).Get("Id") + + var skipMedia bool + err := s.db.QueryRow("SELECT COALESCE(skip_media, false) FROM users WHERE id = $1", txtid).Scan(&skipMedia) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("failed to read skip_media configuration")) + return + } + + response := map[string]interface{}{ + "skip_media": skipMedia, + } + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + } +} + +// SetSkipMedia updates the per-instance skip_media setting. When true, media is +// not automatically downloaded for incoming messages on this instance; set it +// to false (the default) to enable automatic media download. +func (s *server) SetSkipMedia() http.HandlerFunc { + type skipMediaStruct struct { + SkipMedia *bool `json:"skip_media"` + } + + return func(w http.ResponseWriter, r *http.Request) { + txtid := r.Context().Value("userinfo").(Values).Get("Id") + + decoder := json.NewDecoder(r.Body) + var t skipMediaStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("could not decode payload")) + return + } + + if t.SkipMedia == nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("skip_media is required")) + return + } + + // Store skip_media configuration in database + _, err = s.db.Exec("UPDATE users SET skip_media = $1 WHERE id = $2", *t.SkipMedia, txtid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("failed to save skip_media configuration")) + return + } + + token := r.Context().Value("userinfo").(Values).Get("Token") + if cachedUserInfo, found := userinfocache.Get(token); found { + updatedUserInfo := updateUserInfo(cachedUserInfo, "SkipMedia", strconv.FormatBool(*t.SkipMedia)).(Values) + userinfocache.Set(token, updatedUserInfo, cache.NoExpiration) + log.Info().Str("userID", txtid).Bool("skip_media", *t.SkipMedia).Msg("User info cache updated with SkipMedia configuration") + } + + response := map[string]interface{}{ + "Details": "Skip media configured successfully", + "skip_media": *t.SkipMedia, + } + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + } +} + // Set proxy func (s *server) SetProxy() http.HandlerFunc { type proxyStruct struct { diff --git a/migrations.go b/migrations.go index 46f2a3a0..436254ae 100644 --- a/migrations.go +++ b/migrations.go @@ -75,6 +75,11 @@ var migrations = []Migration{ Name: "add_whatsmeow_message_secrets_message_id_idx", UpSQL: addWhatsmeowMessageSecretsMessageIDIndexSQL, }, + { + ID: 10, + Name: "add_skip_media", + UpSQL: addSkipMediaSQL, + }, } const changeIDToStringSQL = ` @@ -219,6 +224,21 @@ END $$; -- SQLite version (handled in code) ` +const addSkipMediaSQL = ` +-- PostgreSQL version +DO $$ +BEGIN + -- Per-instance toggle to skip automatic media download on incoming messages. + -- Defaults to FALSE to preserve existing behavior (media is downloaded + -- unless explicitly disabled for the instance). + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'skip_media') THEN + ALTER TABLE users ADD COLUMN skip_media BOOLEAN DEFAULT FALSE; + END IF; +END $$; + +-- SQLite version (handled in code) +` + const addWhatsmeowMessageSecretsMessageIDIndexSQL = ` -- PostgreSQL version DO $$ @@ -456,6 +476,13 @@ func applyMigration(db *sqlx.DB, migration Migration) error { } else { _, err = tx.Exec(migration.UpSQL) } + } else if migration.ID == 10 { + if db.DriverName() == "sqlite" { + // Per-instance skip_media toggle, defaults to disabled (0 = download). + err = addColumnIfNotExistsSQLite(tx, "users", "skip_media", "BOOLEAN DEFAULT 0") + } else { + _, err = tx.Exec(migration.UpSQL) + } } else { _, err = tx.Exec(migration.UpSQL) } diff --git a/routes.go b/routes.go index 21b40251..74d492e2 100644 --- a/routes.go +++ b/routes.go @@ -94,6 +94,9 @@ func (s *server) routes() { s.router.Handle("/session/proxy", c.Then(s.SetProxy())).Methods("POST") s.router.Handle("/session/history", c.Then(s.SetHistory())).Methods("POST") + s.router.Handle("/session/skipmedia", c.Then(s.GetSkipMedia())).Methods("GET") + s.router.Handle("/session/skipmedia", c.Then(s.SetSkipMedia())).Methods("POST") + s.router.Handle("/session/s3/config", c.Then(s.ConfigureS3())).Methods("POST") s.router.Handle("/session/s3/config", c.Then(s.GetS3Config())).Methods("GET") s.router.Handle("/session/s3/config", c.Then(s.DeleteS3Config())).Methods("DELETE") diff --git a/static/api/spec.yml b/static/api/spec.yml index c21c9730..c899dd58 100644 --- a/static/api/spec.yml +++ b/static/api/spec.yml @@ -675,6 +675,89 @@ paths: description: Bad request or session not connected '500': description: Internal server error + /session/skipmedia: + get: + tags: + - Session + summary: Get media download setting + description: >- + Returns the per-instance `skip_media` setting. When `true` the server + does not automatically download media from incoming messages for this + instance. Defaults to `false` (media is downloaded). + security: + - ApiKeyAuth: [] + responses: + '200': + description: Current skip_media setting + content: + application/json: + schema: + type: object + properties: + code: + type: integer + example: 200 + success: + type: boolean + example: true + data: + type: object + properties: + skip_media: + type: boolean + example: false + '500': + description: Internal server error + post: + tags: + - Session + summary: Set media download setting + description: >- + Updates the per-instance `skip_media` setting. Set `skip_media` to + `true` to disable automatic media download for incoming messages on this + instance, or `false` (the default) to enable it. + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - skip_media + properties: + skip_media: + type: boolean + description: true to skip downloading media, false to download automatically (default) + example: true + responses: + '200': + description: skip_media setting updated successfully + content: + application/json: + schema: + type: object + properties: + code: + type: integer + example: 200 + success: + type: boolean + example: true + data: + type: object + properties: + Details: + type: string + example: 'Skip media configured successfully' + skip_media: + type: boolean + example: true + '400': + description: Bad request (missing or invalid skip_media) + '500': + description: Internal server error /session/hmac/config: post: tags: diff --git a/static/dashboard/index.html b/static/dashboard/index.html index 621fe66c..bc911951 100644 --- a/static/dashboard/index.html +++ b/static/dashboard/index.html @@ -151,6 +151,12 @@

Instances Management

Configure HMAC key for webhook security.
+
User
@@ -486,6 +492,31 @@

+ + + + +
+
+ + +
+ When enabled, this instance does not automatically download media from incoming messages. Disabled by default. +

Proxy Configuration (Optional)

diff --git a/static/dashboard/js/app.js b/static/dashboard/js/app.js index defe1e7d..5712fd4e 100644 --- a/static/dashboard/js/app.js +++ b/static/dashboard/js/app.js @@ -56,6 +56,9 @@ document.addEventListener('DOMContentLoaded', function() { $('#s3MediaDelivery').dropdown(); $('#addInstanceS3MediaDelivery').dropdown(); + // Initialize media download toggle + $('#skipMediaToggle').checkbox(); + // Initialize proxy enabled checkbox with onChange handler $('#proxyEnabledToggle').checkbox({ onChange: function() { @@ -119,6 +122,9 @@ document.addEventListener('DOMContentLoaded', function() { } }); + // Initialize add instance skip media toggle + $('#addInstanceSkipMediaToggle').checkbox(); + // Handle admin login button click adminLoginBtn.addEventListener('click', function() { isAdminLogin = true; @@ -300,6 +306,17 @@ document.addEventListener('DOMContentLoaded', function() { loadHistoryConfig(); }); + // Media Download Configuration + document.getElementById('skipMediaConfig').addEventListener('click', function() { + $('#modalSkipMediaConfig').modal({ + onApprove: function() { + saveSkipMediaConfig(); + return false; + } + }).modal('show'); + loadSkipMediaConfig(); + }); + // Proxy Configuration document.getElementById('proxyConfig').addEventListener('click', function() { $('#modalProxyConfig').modal({ @@ -494,6 +511,7 @@ document.addEventListener('DOMContentLoaded', function() { $('#addInstanceProxyToggle').checkbox('set unchecked'); $('#addInstanceS3Toggle').checkbox('set unchecked'); $('#addInstanceHmacToggle').checkbox('set unchecked'); + $('#addInstanceSkipMediaToggle').checkbox('set unchecked'); $('#addInstanceProxyUrlField').hide(); $('#addInstanceS3Fields').hide(); $('#addInstanceHmacKeyWarningMessage').hide(); @@ -538,6 +556,8 @@ async function addInstance(data) { const hmacEnabled = data.hmac_enabled === 'on' || data.hmac_enabled === true; const hmacKey = hmacEnabled ? (data.hmac_key || '') : ''; + const skipMedia = data.skip_media === 'on' || data.skip_media === true; + const payload = { name: data.name, token: data.token, @@ -545,6 +565,7 @@ async function addInstance(data) { webhook: data.webhook_url || '', expiration: 0, history: parseInt(data.history) || 0, + skipMedia: skipMedia, proxyConfig: proxyConfig, s3Config: s3Config, hmacKey: hmacKey @@ -1663,6 +1684,65 @@ async function saveHistoryConfig() { } } +// Media Download (skip_media) Configuration Functions +async function loadSkipMediaConfig() { + const token = getLocalStorageItem('token'); + const myHeaders = new Headers(); + myHeaders.append('token', token); + + try { + const res = await fetch(baseUrl + "/session/status", { + method: "GET", + headers: myHeaders + }); + + if (res.ok) { + const data = await res.json(); + // skip_media defaults to false (media download enabled) when unset. + let skipMedia = false; + if (data.code === 200 && data.data && typeof data.data.skip_media !== 'undefined') { + skipMedia = data.data.skip_media; + } + // The toggle represents "download media", i.e. the inverse of skip_media. + $('#downloadMediaEnabled').prop('checked', !skipMedia); + } + } catch (error) { + console.error('Error loading media download config:', error); + } +} + +async function saveSkipMediaConfig() { + const token = getLocalStorageItem('token'); + const myHeaders = new Headers(); + myHeaders.append('token', token); + myHeaders.append('Content-Type', 'application/json'); + + const downloadEnabled = $('#downloadMediaEnabled').is(':checked'); + + const config = { + skip_media: !downloadEnabled, + }; + + try { + const res = await fetch(baseUrl + "/session/skipmedia", { + method: "POST", + headers: myHeaders, + body: JSON.stringify(config) + }); + + const data = await res.json(); + if (data.success) { + showSuccess('Media download configuration saved successfully'); + $('#modalSkipMediaConfig').modal('hide'); + } else { + showError('Failed to save media download configuration: ' + (data.error || 'Unknown error')); + } + } catch (error) { + showError('Error saving media download configuration'); + console.error('Error:', error); + } +} + // Proxy Configuration Functions async function loadProxyConfig() { const token = getLocalStorageItem('token'); diff --git a/static/index.html b/static/index.html index a9d5895e..ddc30471 100644 --- a/static/index.html +++ b/static/index.html @@ -281,7 +281,7 @@

Execution

  • -wadebug: enables whatsmeow debug, INFO or DEBUG levels are supported
  • -sslcertificate: SSL Certificate File
  • -sslprivatekey: SSL Private Key File
  • -
  • -skipmedia: Do not automatically download media from received messages
  • +
  • -skipmedia: Do not automatically download media from received messages (global; can also be configured per instance via the /session/skipmedia endpoint, which defaults to downloading media)
  • -osname: Connection OSName in Whatsapp (default "Mac OS 10")
  • -admintoken: Security Token to authorize admin actions (list/create/remove users)
  • diff --git a/wmiau.go b/wmiau.go index feb69e5a..a476bd0e 100644 --- a/wmiau.go +++ b/wmiau.go @@ -240,7 +240,7 @@ func checkIfSubscribedToEvent(subscribedEvents []string, eventType string, userI // Connects to Whatsapp Websocket on server startup if last state was connected func (s *server) connectOnStartup() { - rows, err := s.db.Queryx("SELECT id,name,token,jid,webhook,events,proxy_url,CASE WHEN s3_enabled THEN 'true' ELSE 'false' END AS s3_enabled,media_delivery,COALESCE(history, 0) as history,hmac_key FROM users WHERE connected=1") + rows, err := s.db.Queryx("SELECT id,name,token,jid,webhook,events,proxy_url,CASE WHEN s3_enabled THEN 'true' ELSE 'false' END AS s3_enabled,media_delivery,COALESCE(history, 0) as history,hmac_key,CASE WHEN COALESCE(skip_media, false) THEN 'true' ELSE 'false' END AS skip_media FROM users WHERE connected=1") if err != nil { log.Error().Err(err).Msg("DB Problem") return @@ -256,9 +256,10 @@ func (s *server) connectOnStartup() { proxy_url := "" s3_enabled := "" media_delivery := "" + skip_media := "" var history int var hmac_key []byte - err = rows.Scan(&txtid, &name, &token, &jid, &webhook, &events, &proxy_url, &s3_enabled, &media_delivery, &history, &hmac_key) + err = rows.Scan(&txtid, &name, &token, &jid, &webhook, &events, &proxy_url, &s3_enabled, &media_delivery, &history, &hmac_key, &skip_media) if err != nil { log.Error().Err(err).Msg("DB Problem") return @@ -279,6 +280,7 @@ func (s *server) connectOnStartup() { "Events": events, "S3Enabled": s3_enabled, "MediaDelivery": media_delivery, + "SkipMedia": skip_media, "History": fmt.Sprintf("%d", history), "HmacKeyEncrypted": hmacKeyEncrypted, }} @@ -831,20 +833,25 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { var s3Config struct { Enabled string `db:"s3_enabled"` MediaDelivery string `db:"media_delivery"` + SkipMedia bool `db:"skip_media"` } lastMessageCache.Set(mycli.userID, &evt.Info, cache.DefaultExpiration) myuserinfo, found := userinfocache.Get(mycli.token) if !found { - err := mycli.db.Get(&s3Config, "SELECT CASE WHEN s3_enabled = 1 THEN 'true' ELSE 'false' END AS s3_enabled, media_delivery FROM users WHERE id = $1", txtid) + err := mycli.db.Get(&s3Config, "SELECT CASE WHEN s3_enabled THEN 'true' ELSE 'false' END AS s3_enabled, media_delivery, COALESCE(skip_media, false) AS skip_media FROM users WHERE id = $1", txtid) if err != nil { log.Error().Err(err).Msg("onMessage Failed to get S3 config from DB as it was not on cache") s3Config.Enabled = "false" s3Config.MediaDelivery = "base64" + s3Config.SkipMedia = false } } else { s3Config.Enabled = myuserinfo.(Values).Get("S3Enabled") s3Config.MediaDelivery = myuserinfo.(Values).Get("MediaDelivery") + // Default to downloading media when the value is missing/unset so the + // per-instance toggle preserves the existing behavior. + s3Config.SkipMedia = myuserinfo.(Values).Get("SkipMedia") == "true" } // Lazy init S3 client if needed (handles reconnect-after-restart when connectOnStartup skipped this user) @@ -931,7 +938,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { } } - if !*skipMedia { + // Skip media download when either the global -skipmedia flag is set or + // the per-instance skip_media setting is enabled. Both default to off, + // so media is downloaded unless explicitly disabled. + if !*skipMedia && !s3Config.SkipMedia { isIncoming := !evt.Info.IsFromMe chatJID := evt.Info.Sender.String()