Eliminación de subida vía API

Intenté acceder a él, pero parece que no existe un endpoint de la API para eliminar una subida no existe.

Muppet Show: Kermit's Trapeze Fall

Lo publico aquí porque no pude encontrar ninguna mención o sugerencia al respecto; lo más parecido que encontré fue: API: a scope with access to /uploads

En la configuración de alcance granular de la clave de API para las subidas, veo que ‘create’ (crear) está disponible, pero ‘delete’ (eliminar) no lo está.

Parece que una posible solución alternativa podría ser crear una automatización personalizada y activarla a través de la API.

4 Me gusta

¿Cuál es su caso de uso?

Dependiendo de su configuración, dado que las subidas son un recurso compartido (referenciado por publicaciones, perfil de usuario, insignias, etc.), es algo arriesgado eliminarlas sin la debida diligencia.

Además, se limpian automáticamente después de un tiempo si no están siendo referenciadas en ninguna parte.

3 Me gusta

El caso de uso es para la eliminación urgente de subidas como parte de flujos de trabajo de automatización con intervención humana utilizando activepieces, data explorer y la API.

El objetivo es permitir que los moderadores habituales tengan acceso a una forma sencilla de eliminar de forma segura y completa las subidas inmediatamente sin acceso SSH y cubrir exhaustivamente todos los casos conocidos de rotura de referencias de subidas, además de purgar automáticamente la CDN de las URL específicas (y para el caso de avatares proxificados, el prefijo de URL basado en el nombre de usuario).


Me sentí aliviado al ver en las pruebas que las referencias de avatares, fondos de perfil y fondos de tarjeta se manejan automáticamente cuando se destruye una subida.

https://github.com/discourse/discourse/commit/e1975e293f2625259e925b4a3c93d88d5acfcaa8

https://github.com/discourse/discourse/commit/38e7b1a0492dd4282c3cd3b1ddb2b3343661d31f


Los dos escenarios principales serían ‘tierra quemada’ y ‘eliminación quirúrgica’. Este es el trabajo en curso para tierra quemada:


Conversión a una lista de hashes de subidas:

  • URL de avatar (extraer nombre de usuario usando regex) → obtener hash de avatar mediante consulta de data explorer (el hash no está en la URL)

  • URL de tema/publicación → recopilar todos los hashes de subida utilizados en esa publicación usando data explorer

  • URL de subida original/optimizada directa (incluido el fondo de perfil y el fondo de tarjeta) → extraer hashes usando regex


Luego consultar cada hash para encontrar todas las ocurrencias (una consulta de data explorer para cubrir todos los casos, un hash a la vez):

  • lista de nombre de usuario/ID, para usuarios que lo usaron como avatar

  • lista de nombre de usuario/ID, para usuarios que lo usaron como fondo de perfil

  • lista de nombre de usuario/ID, para usuarios que lo usaron como fondo de tarjeta

  • lista de todas las publicaciones (raw) que usan esta subida


Acciones:

  • suspender a todos los usuarios que usaron la subida para un avatar, fondo de perfil o fondo de tarjeta

  • suspender a todos los usuarios que tienen publicaciones que hacen referencia a la subida objetivo, pero excluir si su referencia está anidada dentro de una cita

  • eliminar todos los temas

  • eliminar todas las publicaciones

  • destruir subida (no hay endpoint de API)

  • purgar todas las URL de CDN (optimizadas/no optimizadas)

  • purgar el prefijo estándar para las URL de avatar proxificadas para cada nombre de usuario asociado (para cubrir todos los tamaños)


El escenario de eliminación quirúrgica sería esencialmente el mismo, pero sin suspender a los usuarios asociados y necesitaría algunos cambios con respecto a la recopilación de todos los hashes de subida de las URL de publicaciones/temas.

Probablemente seguiría siendo necesario eliminar las publicaciones/temas en sí para evitar referencias rotas, pero eliminar el markdown de la subida para esa subida específica (sin tocar otras subidas) sería superior si es posible. Como esta automatización si no eliminara todas las referencias de subidas en markdown.


Idealmente, me gustaría poder bloquear también los hashes, de modo que después de pasar por lo anterior, alguien no pueda simplemente crear una nueva cuenta y volver a subir. :melting_face: :coffin:

No creo que sea posible actualmente para un moderador normal, como usando palabras vigiladas. Así que quizás realizar un escaneo periódico como el anterior para una lista de hashes sería una forma de manejar eso.

1 me gusta

¿Has revisado nuestro Legal Compliance Plugin?

Tiene endpoints para obtener las subidas (uploads) de una publicación y para eliminar una subida dados los identificadores (IDs) de la subida y la publicación.

También extiende la funcionalidad de búsqueda para obtener todas las publicaciones que contienen una subida específica, dado el hash.

2 Me gusta

Solo estoy enlazando esto para los usuarios que lean y estén de acuerdo con tu publicación, podrían estar interesados en votar por esto (sé que ya lo leíste):

2 Me gusta

¡Sí! Lloré lágrimas de alegría al ver tu plugin a principios de este mes, jaja. Muchísimas gracias por crearlo y compartirlo. :heart:

Algunos escenarios que no encontré cómo cubrir con él:

  • Usando la subida: prefijo de búsqueda, no parecía posible encontrar fondos de perfil o fondos de tarjeta (a menos que alguien vinculara directamente esas imágenes en un post también).
  • Eliminar permanentemente imágenes en ese escenario (imagen sin asociación con posts).
  • Similar para el Avatar, por ejemplo, obtener el hash del avatar (normalmente no está en la URL), luego encontrar cualquier otra cuenta que posiblemente también lo haya usado, si se necesita una suspensión + eliminar la subida sin asociación de post.

Cosas agradables de tener:

  • Activar un webhook tras la eliminación, para activar purgas de CDN después de la eliminación para todas las variantes de las subidas.
  • Eliminar masivamente/automáticamente todos los posts/topics que tengan la referencia de subida cuando se elimina la subida.

En Discourse (interfaz web), no parece posible eliminar (desvincular) el avatar de un usuario, ni por un administrador/moderador ni por el propio usuario (además de subir un reemplazo). Es posible para el fondo de perfil y el banner. Pero ambos no destruyen la subida inmediatamente y si existe otro usuario desconocido que usa la misma subida para una parte de su perfil (avatar, fondo de perfil, tarjeta de perfil) tampoco se purgará más tarde.


Terminé pensando que podría valer la pena intentar crear algunos flujos de trabajo automatizados que puedan manejar la mayor parte o todo el proceso (con revisión/aprobación humana y/o entrada humana simplificada). Para facilitar su aplicación de manera consistente, cubrir casos extremos e idealmente minimizar la posibilidad de tener posts no eliminados que tengan referencias de subida muertas. También suspender automáticamente si es necesario (a menos que la subida objetivo esté dentro de una cita) y purgar la CDN.


Esto es lo que tenía hasta ahora para las consultas del explorador de datos (aún no en la parte de ‘unirlas’), con el objetivo de cubrir:

  • URLs de temas (topic urls)
  • URLs de posts
  • URLs de imágenes directas (fondo de post, fondo de perfil, fondo de tarjeta)
  • URLs de avatares (proxificadas)

Preparar Lista de Hashes de Subidas

nombredeusuario (username) (desde URL de avatar proxificada) a hash de subida

-- [params]
-- user_id :username
SELECT
    up.sha1 AS upload_hash
FROM
    users u
    JOIN uploads up ON up.id = u.uploaded_avatar_id
WHERE
    u.id = :username

URL de post/topic a lista de hashes de subida

-- [params]
-- post_id :post_url
SELECT
    COALESCE(json_agg(up.sha1), '[]'::json) AS upload_hashes
FROM
    upload_references ur
    JOIN uploads up ON up.id = ur.upload_id
WHERE
    ur.target_id = :post_url
    AND ur.target_type = 'Post'

Los otros tipos de URL de subida de los que estoy al tanto, tienen el hash en la propia URL (no es necesario usar el explorador de datos para obtener esos hashes).


Recopilar y preparar toda la información procesable sobre un hash de subida

parámetros de ejemplo:

upload_schemeless_prefix:
//my-s3-bucket.somehost.example.com

cdn_url_prefix:
https://cdn.example.com

app_hostname:
www.example.com

upload_hash:
0000000000000000000000000000000000000000

búsqueda y resumen de hash

-- [params]
-- string :upload_hash
-- string :upload_schemeless_prefix
-- string :cdn_url_prefix
-- string :app_hostname
WITH target_uploads AS (
    SELECT
        id,
        url,
        user_id
    FROM
        uploads
    WHERE
        sha1 = :upload_hash
),
all_urls AS (
    SELECT
        url
    FROM
        target_uploads
    UNION
    SELECT
        url
    FROM
        optimized_images
    WHERE
        upload_id IN (
            SELECT
                id
            FROM
                target_uploads)
),
post_data AS (
    SELECT
        p.id AS post_id,
        p.topic_id,
        u.id AS user_id,
        u.username AS author,
        p.created_at,
        CASE WHEN p.post_number = 1 THEN
            TRUE
        ELSE
            FALSE
        END AS is_topic_starter,
        CASE WHEN t.archetype = 'private_message' THEN
            TRUE
        ELSE
            FALSE
        END AS is_dm,
        p.raw
    FROM
        upload_references ur
        JOIN posts p ON p.id = ur.target_id
            AND ur.target_type = 'Post'
        JOIN topics t ON t.id = p.topic_id
        JOIN users u ON u.id = p.user_id
    WHERE
        ur.upload_id IN (
            SELECT
                id
            FROM
                target_uploads)
            AND p.deleted_at IS NULL
)
SELECT
    (
        SELECT
            COALESCE(json_agg(id), '[]'::json)
        FROM
            target_uploads) AS upload_ids,
    (
        SELECT
            COALESCE(json_agg(url), '[]'::json)
        FROM
            all_urls) AS upload_s3_schemeless_urls,
    (
        SELECT
            COALESCE(json_agg(
                    CASE WHEN url LIKE '//%' THEN
                        REPLACE(url, :upload_schemeless_prefix, :cdn_url_prefix)
                    ELSE
                        :cdn_url_prefix || url
                    END), '[]'::json)
        FROM
            all_urls) AS upload_cdn_urls,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', id, 'username', username)), '[]'::json)
        FROM
            users
        WHERE
            uploaded_avatar_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS avatar_users,
    (
        SELECT
            COALESCE(json_agg('https://' || :app_hostname || '/user_avatar/' || :app_hostname || '/' || username || '/'), '[]'::json)
        FROM
            users
        WHERE
            uploaded_avatar_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS avatar_proxied_url_prefixes,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', u.id, 'username', u.username)), '[]'::json)
        FROM
            user_profiles up
            JOIN users u ON u.id = up.user_id
        WHERE
            up.profile_background_upload_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS profile_background_users,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', u.id, 'username', u.username)), '[]'::json)
        FROM
            user_profiles up
            JOIN users u ON u.id = up.user_id
        WHERE
            up.card_background_upload_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS card_background_users,
    (
        SELECT
            COALESCE(json_agg(pd.topic_id), '[]'::json)
        FROM
            post_data pd
        WHERE
            pd.is_topic_starter = TRUE) AS topic_ids,
    (
        SELECT
            COALESCE(json_agg(pd.post_id), '[]'::json)
        FROM
            post_data pd
        WHERE
            pd.is_topic_starter = FALSE) AS post_ids,
    (
        SELECT
            COALESCE(json_agg(json_build_object('post_id', pd.post_id, 'topic_id', pd.topic_id, 'user_id', pd.user_id, 'author', pd.author, 'created_at', pd.created_at, 'is_topic_starter', pd.is_topic_starter, 'is_dm', pd.is_dm, 'raw', pd.raw)), '[]'::json)
        FROM
            post_data pd) AS post_details

No cubre: topics en cola de revisión pendientes o posts en borrador

Salidas:

upload_ids (solo debería haber uno)
upload_s3_schemeless_urls
upload_cdn_urls
avatar_users
avatar_proxied_url_prefixes
profile_background_users
card_background_users
topic_ids
post_ids
post_details

Eso proporcionará la información necesaria para usar estas API selectivamente:

Y para acciones adicionales:

  • Purgar todas las URLs de CDN (todas las variantes, optimizadas y originales, etc.)
  • Purgar prefijos de URL de avatares proxificados (para cubrir todos los tamaños)

Manejado automáticamente por Discourse:

  • Elimina la referencia de subida de todos los perfiles de usuario (avatar, fondo de perfil y fondo de tarjeta) inmediatamente cuando se destruye/elimina una subida.
1 me gusta