Plugin Directory

Changeset 3757656


Ignore:
Timestamp:
10/21/2026 02:45:45 AM (3 weeks ago)
Author:
wvdevel
Message:

Release 1.3.0: Entregas (recepcion de archivos por el bot)

Location:
wvdevel-community-assistant-telegram
Files:
8 added
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • wvdevel-community-assistant-telegram/tags/1.3.0/admin/class-wvdevel-admin.php

    r3687494 r3757656  
    2020                add_action( 'admin_post_wvdevel_upload_resource', array( __CLASS__, 'upload_resource' ) );
    2121                add_action( 'admin_post_wvdevel_delete_resource', array( __CLASS__, 'delete_resource' ) );
     22                add_action( 'admin_post_wvdevel_save_entregas', array( __CLASS__, 'save_entregas' ) );
     23                add_action( 'admin_post_wvdevel_save_delivery', array( __CLASS__, 'save_delivery' ) );
     24                add_action( 'admin_post_wvdevel_delete_delivery', array( __CLASS__, 'delete_delivery' ) );
     25                add_action( 'admin_post_wvdevel_toggle_delivery', array( __CLASS__, 'toggle_delivery' ) );
     26                add_action( 'admin_post_wvdevel_download_submission', array( __CLASS__, 'download_submission' ) );
     27                add_action( 'admin_post_wvdevel_delete_submission', array( __CLASS__, 'delete_submission' ) );
    2228
    2329                add_action( 'wp_ajax_wvdevel_setup_webhook', array( __CLASS__, 'ajax_setup_webhook' ) );
    … …  
    4349                add_submenu_page( 'wvdevel-settings', __( 'Difusion (Broadcast)', 'wvdevel-community-assistant-telegram' ), __( 'Difusion (Broadcast)', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-broadcast', array( __CLASS__, 'render_broadcast_page' ) );
    4450                add_submenu_page( 'wvdevel-settings', __( 'Recursos por usuario', 'wvdevel-community-assistant-telegram' ), __( 'Recursos por usuario', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-resources', array( __CLASS__, 'render_resources_page' ) );
     51                add_submenu_page( 'wvdevel-settings', __( 'Entregas', 'wvdevel-community-assistant-telegram' ), __( 'Entregas', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-deliveries', array( __CLASS__, 'render_deliveries_page' ) );
     52                add_submenu_page( 'wvdevel-settings', __( 'Entregas recibidas', 'wvdevel-community-assistant-telegram' ), __( 'Entregas recibidas', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-submissions', array( __CLASS__, 'render_submissions_page' ) );
    4553        }
    4654
    … …  
    110118                }
    111119                require WVDEVEL_PLUGIN_DIR . 'admin/views/resources-page.php';
     120        }
     121
     122        public static function render_deliveries_page() {
     123                if ( ! current_user_can( 'manage_options' ) ) {
     124                        return;
     125                }
     126                require WVDEVEL_PLUGIN_DIR . 'admin/views/deliveries-page.php';
     127        }
     128
     129        public static function render_submissions_page() {
     130                if ( ! current_user_can( 'manage_options' ) ) {
     131                        return;
     132                }
     133                require WVDEVEL_PLUGIN_DIR . 'admin/views/submissions-page.php';
    112134        }
    113135
    … …  
    498520                exit;
    499521        }
     522
     523        public static function save_entregas() {
     524                if ( ! current_user_can( 'manage_options' ) ) {
     525                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     526                }
     527                check_admin_referer( 'wvdevel_save_entregas' );
     528
     529                WVDEVEL_Settings::update(
     530                        array(
     531                                'entregas_enabled' => ! empty( $_POST['entregas_enabled'] ),
     532                                'entregas_max_mb'  => max( 1, min( 20, absint( wp_unslash( $_POST['entregas_max_mb'] ?? 20 ) ) ) ),
     533                        )
     534                );
     535                if ( ! empty( $_POST['entregas_enabled'] ) ) {
     536                        WVDEVEL_Submissions::ensure_protected_dir();
     537                }
     538
     539                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'saved' ), admin_url( 'admin.php' ) ) );
     540                exit;
     541        }
     542
     543        public static function save_delivery() {
     544                if ( ! current_user_can( 'manage_options' ) ) {
     545                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     546                }
     547                check_admin_referer( 'wvdevel_save_delivery' );
     548
     549                $id      = absint( wp_unslash( $_POST['delivery_id'] ?? 0 ) );
     550                $slug    = WVDEVEL_Submissions::sanitize_slug( wp_unslash( $_POST['slug'] ?? '' ) );
     551                $label   = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) );
     552                $pattern = sanitize_text_field( wp_unslash( $_POST['pattern'] ?? '' ) );
     553                $max_mb  = max( 1, min( 20, absint( wp_unslash( $_POST['max_mb'] ?? 20 ) ) ) );
     554                $prefix  = ! empty( $_POST['require_carnet_prefix'] ) ? 1 : 0;
     555                $is_open = ! empty( $_POST['is_open'] ) ? 1 : 0;
     556                $opens   = self::clean_datetime( wp_unslash( $_POST['opens_at'] ?? '' ) );
     557                $closes  = self::clean_datetime( wp_unslash( $_POST['closes_at'] ?? '' ) );
     558
     559                if ( '' === $slug || '' === $label ) {
     560                        wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_error' ), admin_url( 'admin.php' ) ) );
     561                        exit;
     562                }
     563
     564                $data = array(
     565                        'slug'                  => $slug,
     566                        'label'                 => $label,
     567                        'pattern'               => $pattern,
     568                        'max_mb'                => $max_mb,
     569                        'require_carnet_prefix' => $prefix,
     570                        'is_open'               => $is_open,
     571                        'opens_at'              => $opens,
     572                        'closes_at'             => $closes,
     573                );
     574
     575                if ( $id ) {
     576                        WVDEVEL_DB::update_delivery( $id, $data );
     577                } else {
     578                        $existing = WVDEVEL_DB::get_delivery_by_slug( $slug );
     579                        if ( $existing ) {
     580                                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_dup' ), admin_url( 'admin.php' ) ) );
     581                                exit;
     582                        }
     583                        WVDEVEL_DB::insert_delivery( $data );
     584                }
     585
     586                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'saved' ), admin_url( 'admin.php' ) ) );
     587                exit;
     588        }
     589
     590        /** Returns a Y-m-d H:i:s string or null for an empty/invalid datetime-local value. */
     591        private static function clean_datetime( $raw ) {
     592                $raw = trim( (string) $raw );
     593                if ( '' === $raw ) {
     594                        return null;
     595                }
     596                $ts = strtotime( $raw );
     597                return false === $ts ? null : gmdate( 'Y-m-d H:i:s', $ts );
     598        }
     599
     600        public static function delete_delivery() {
     601                if ( ! current_user_can( 'manage_options' ) ) {
     602                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     603                }
     604                check_admin_referer( 'wvdevel_delete_delivery' );
     605
     606                $id = absint( wp_unslash( $_GET['delivery_id'] ?? 0 ) );
     607                if ( $id ) {
     608                        WVDEVEL_DB::delete_delivery( $id );
     609                }
     610
     611                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_deleted' ), admin_url( 'admin.php' ) ) );
     612                exit;
     613        }
     614
     615        public static function toggle_delivery() {
     616                if ( ! current_user_can( 'manage_options' ) ) {
     617                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     618                }
     619                check_admin_referer( 'wvdevel_toggle_delivery' );
     620
     621                $id   = absint( wp_unslash( $_GET['delivery_id'] ?? 0 ) );
     622                $open = ! empty( $_GET['open'] ) ? 1 : 0;
     623                if ( $id ) {
     624                        WVDEVEL_DB::update_delivery( $id, array( 'is_open' => $open ) );
     625                }
     626
     627                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'updated' ), admin_url( 'admin.php' ) ) );
     628                exit;
     629        }
     630
     631        public static function delete_submission() {
     632                if ( ! current_user_can( 'manage_options' ) ) {
     633                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     634                }
     635                check_admin_referer( 'wvdevel_delete_submission' );
     636
     637                $id = absint( wp_unslash( $_GET['submission_id'] ?? 0 ) );
     638                if ( $id ) {
     639                        WVDEVEL_Submissions::delete( $id );
     640                }
     641
     642                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-submissions', 'wvdevel_notice' => 'submission_deleted' ), admin_url( 'admin.php' ) ) );
     643                exit;
     644        }
     645
     646        /** Streams a received submission to the admin (files are not web-accessible). */
     647        public static function download_submission() {
     648                if ( ! current_user_can( 'manage_options' ) ) {
     649                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     650                }
     651                check_admin_referer( 'wvdevel_download_submission' );
     652
     653                $id  = absint( wp_unslash( $_GET['submission_id'] ?? 0 ) );
     654                $row = $id ? WVDEVEL_DB::get_submission( $id ) : null;
     655                if ( ! $row ) {
     656                        wp_die( esc_html__( 'Entrega no encontrada.', 'wvdevel-community-assistant-telegram' ) );
     657                }
     658                $path = WVDEVEL_Submissions::file_path( $row );
     659                if ( '' === $path || ! is_readable( $path ) ) {
     660                        wp_die( esc_html__( 'No se encontro el archivo en el servidor.', 'wvdevel-community-assistant-telegram' ) );
     661                }
     662
     663                $name = sanitize_file_name( $row->original_name );
     664                if ( '' === $name ) {
     665                        $name = 'entrega';
     666                }
     667
     668                nocache_headers();
     669                header( 'Content-Type: application/octet-stream' );
     670                header( 'Content-Disposition: attachment; filename="' . $name . '"' );
     671                header( 'Content-Length: ' . (int) filesize( $path ) );
     672
     673                while ( ob_get_level() ) {
     674                        ob_end_clean();
     675                }
     676                // phpcs:ignore WordPress.WP.AlternativeFunctions -- streaming a private, access-guarded plugin file to a capability-checked, nonce-verified admin; WordPress has no API for a raw file download.
     677                readfile( $path );
     678                exit;
     679        }
     680
    500681}
  • wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-activator.php

    r3669621 r3757656  
    3737                $log_table       = $wpdb->prefix . 'wvdevel_message_log';
    3838                $resources_table = $wpdb->prefix . 'wvdevel_resources';
     39                $deliveries_table  = $wpdb->prefix . 'wvdevel_deliveries';
     40                $submissions_table = $wpdb->prefix . 'wvdevel_submissions';
    3941
    4042                $sql = "CREATE TABLE {$users_table} (
    … …  
    8688                        PRIMARY KEY  (id),
    8789                        KEY carnet (carnet)
     90                ) {$charset_collate};
     91
     92                CREATE TABLE {$deliveries_table} (
     93                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     94                        slug VARCHAR(100) NOT NULL,
     95                        label VARCHAR(191) NOT NULL,
     96                        pattern VARCHAR(255) NULL,
     97                        max_mb SMALLINT UNSIGNED NOT NULL DEFAULT 20,
     98                        require_carnet_prefix TINYINT(1) NOT NULL DEFAULT 0,
     99                        is_open TINYINT(1) NOT NULL DEFAULT 1,
     100                        opens_at DATETIME NULL,
     101                        closes_at DATETIME NULL,
     102                        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     103                        PRIMARY KEY  (id),
     104                        UNIQUE KEY slug (slug)
     105                ) {$charset_collate};
     106
     107                CREATE TABLE {$submissions_table} (
     108                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     109                        delivery_slug VARCHAR(100) NOT NULL,
     110                        carnet VARCHAR(191) NOT NULL,
     111                        telegram_id BIGINT NOT NULL,
     112                        stored_name VARCHAR(255) NOT NULL,
     113                        original_name VARCHAR(255) NOT NULL,
     114                        mime VARCHAR(100) NULL,
     115                        size BIGINT UNSIGNED NOT NULL DEFAULT 0,
     116                        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     117                        PRIMARY KEY  (id),
     118                        KEY delivery_slug (delivery_slug),
     119                        KEY carnet (carnet)
    88120                ) {$charset_collate};";
    89121
    … …  
    102134
    103135                WVDEVEL_Resources::ensure_protected_dir();
     136                WVDEVEL_Submissions::ensure_protected_dir();
    104137        }
    105138}
  • wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-db.php

    r3669621 r3757656  
    383383                // phpcs:enable
    384384        }
     385
     386        // ---------------------------------------------------------------------
     387        // Entregas (deliveries) and their submissions.
     388        // ---------------------------------------------------------------------
     389
     390        public static function table_deliveries() {
     391                global $wpdb;
     392                return $wpdb->prefix . 'wvdevel_deliveries';
     393        }
     394
     395        public static function table_submissions() {
     396                global $wpdb;
     397                return $wpdb->prefix . 'wvdevel_submissions';
     398        }
     399
     400        /** Deliveries that are open right now (is_open plus the opens/closes window). */
     401        public static function get_open_deliveries() {
     402                global $wpdb;
     403                $table = self::table_deliveries();
     404                $now   = current_time( 'mysql' );
     405                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name, not user input; values parameterized via prepare().
     406                return $wpdb->get_results(
     407                        $wpdb->prepare(
     408                                "SELECT * FROM {$table} WHERE is_open = 1 AND ( opens_at IS NULL OR opens_at <= %s ) AND ( closes_at IS NULL OR closes_at >= %s ) ORDER BY label ASC",
     409                                $now,
     410                                $now
     411                        )
     412                );
     413        }
     414
     415        public static function get_all_deliveries() {
     416                global $wpdb;
     417                $table = self::table_deliveries();
     418                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; static admin list.
     419                return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY is_open DESC, label ASC" );
     420        }
     421
     422        public static function get_delivery( $id ) {
     423                global $wpdb;
     424                $table = self::table_deliveries();
     425                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     426                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
     427        }
     428
     429        public static function get_delivery_by_slug( $slug ) {
     430                global $wpdb;
     431                $table = self::table_deliveries();
     432                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     433                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE slug = %s", $slug ) );
     434        }
     435
     436        /** Inserts a delivery; returns the new id or 0 (e.g. duplicate slug). */
     437        public static function insert_delivery( array $data ) {
     438                global $wpdb;
     439                $table = self::table_deliveries();
     440                $data  = wp_parse_args(
     441                        $data,
     442                        array(
     443                                'slug'                  => '',
     444                                'label'                 => '',
     445                                'pattern'               => '',
     446                                'max_mb'                => 20,
     447                                'require_carnet_prefix' => 0,
     448                                'is_open'               => 1,
     449                                'opens_at'              => null,
     450                                'closes_at'             => null,
     451                                'created_at'            => current_time( 'mysql' ),
     452                        )
     453                );
     454                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->insert().
     455                $result = $wpdb->insert( $table, $data );
     456                return false !== $result ? (int) $wpdb->insert_id : 0;
     457        }
     458
     459        public static function update_delivery( $id, array $data ) {
     460                global $wpdb;
     461                $table = self::table_deliveries();
     462                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->update().
     463                return false !== $wpdb->update( $table, $data, array( 'id' => (int) $id ) );
     464        }
     465
     466        public static function delete_delivery( $id ) {
     467                global $wpdb;
     468                $table = self::table_deliveries();
     469                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->delete().
     470                return false !== $wpdb->delete( $table, array( 'id' => (int) $id ) );
     471        }
     472
     473        public static function add_submission( $delivery_slug, $carnet, $telegram_id, $stored_name, $original_name, $mime, $size ) {
     474                global $wpdb;
     475                $table = self::table_submissions();
     476                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->insert().
     477                $result = $wpdb->insert(
     478                        $table,
     479                        array(
     480                                'delivery_slug' => $delivery_slug,
     481                                'carnet'        => $carnet,
     482                                'telegram_id'   => (int) $telegram_id,
     483                                'stored_name'   => $stored_name,
     484                                'original_name' => $original_name,
     485                                'mime'          => $mime,
     486                                'size'          => (int) $size,
     487                                'created_at'    => current_time( 'mysql' ),
     488                        )
     489                );
     490                return false !== $result ? (int) $wpdb->insert_id : 0;
     491        }
     492
     493        public static function get_submission( $id ) {
     494                global $wpdb;
     495                $table = self::table_submissions();
     496                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     497                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
     498        }
     499
     500        public static function delete_submission_row( $id ) {
     501                global $wpdb;
     502                $table = self::table_submissions();
     503                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->delete().
     504                return false !== $wpdb->delete( $table, array( 'id' => (int) $id ) );
     505        }
     506
     507        public static function find_submission( $delivery_slug, $carnet, $original_name ) {
     508                global $wpdb;
     509                $table = self::table_submissions();
     510                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     511                return $wpdb->get_row(
     512                        $wpdb->prepare( "SELECT * FROM {$table} WHERE delivery_slug = %s AND carnet = %s AND original_name = %s", $delivery_slug, $carnet, $original_name )
     513                );
     514        }
     515
     516        public static function count_submissions_for( $delivery_slug, $carnet ) {
     517                global $wpdb;
     518                $table = self::table_submissions();
     519                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     520                return (int) $wpdb->get_var(
     521                        $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE delivery_slug = %s AND carnet = %s", $delivery_slug, $carnet )
     522                );
     523        }
     524
     525        public static function get_submissions( array $args = array() ) {
     526                global $wpdb;
     527                $table    = self::table_submissions();
     528                $per_page = isset( $args['per_page'] ) ? (int) $args['per_page'] : 30;
     529                $page     = isset( $args['page'] ) ? max( 1, (int) $args['page'] ) : 1;
     530                $offset   = ( $page - 1 ) * $per_page;
     531                $slug     = isset( $args['delivery_slug'] ) ? trim( (string) $args['delivery_slug'] ) : '';
     532                $search   = isset( $args['search'] ) ? trim( (string) $args['search'] ) : '';
     533
     534                $where  = '1=1';
     535                $params = array();
     536                if ( '' !== $slug ) {
     537                        $where   .= ' AND delivery_slug = %s';
     538                        $params[] = $slug;
     539                }
     540                if ( '' !== $search ) {
     541                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     542                        $where   .= ' AND ( carnet LIKE %s OR original_name LIKE %s )';
     543                        $params[] = $like;
     544                        $params[] = $like;
     545                }
     546                $params[] = $per_page;
     547                $params[] = $offset;
     548
     549                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; all values parameterized via prepare().
     550                return $wpdb->get_results(
     551                        $wpdb->prepare( "SELECT * FROM {$table} WHERE {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d", $params )
     552                );
     553        }
     554
     555        public static function count_submissions( array $args = array() ) {
     556                global $wpdb;
     557                $table  = self::table_submissions();
     558                $slug   = isset( $args['delivery_slug'] ) ? trim( (string) $args['delivery_slug'] ) : '';
     559                $search = isset( $args['search'] ) ? trim( (string) $args['search'] ) : '';
     560
     561                $where  = '1=1';
     562                $params = array();
     563                if ( '' !== $slug ) {
     564                        $where   .= ' AND delivery_slug = %s';
     565                        $params[] = $slug;
     566                }
     567                if ( '' !== $search ) {
     568                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     569                        $where   .= ' AND ( carnet LIKE %s OR original_name LIKE %s )';
     570                        $params[] = $like;
     571                        $params[] = $like;
     572                }
     573
     574                if ( empty( $params ) ) {
     575                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; no user input in this branch.
     576                        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE {$where}" );
     577                }
     578                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     579                return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$where}", $params ) );
     580        }
     581
    385582}
  • wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-message-router.php

    r3687494 r3757656  
    1616                }
    1717
     18                if ( ! empty( $update['message']['document'] ) ) {
     19                        self::handle_document( $update['message'] );
     20                        return;
     21                }
     22
    1823                if ( empty( $update['message'] ) || empty( $update['message']['text'] ) ) {
    19                         return; // Only plain text messages and callback queries are handled in this version.
     24                        return; // Only plain text messages, documents and callback queries are handled.
    2025                }
    2126
    … …  
    143148        }
    144149
     150        /**
     151         * Incoming file (document) from a user. Deliveries are accepted only in a
     152         * private chat with the bot, from validated users (admins bypass the gate).
     153         */
     154        private static function handle_document( array $message ) {
     155                $from = isset( $message['from'] ) ? $message['from'] : array();
     156                $chat = isset( $message['chat'] ) ? $message['chat'] : array();
     157                if ( empty( $from ) || ! empty( $from['is_bot'] ) || empty( $chat['id'] ) ) {
     158                        return;
     159                }
     160                if ( ! isset( $chat['type'] ) || 'private' !== $chat['type'] ) {
     161                        return; // Files are only accepted privately, never in groups.
     162                }
     163                if ( ! WVDEVEL_Submissions::is_enabled() ) {
     164                        return;
     165                }
     166
     167                $telegram_id = $from['id'];
     168                $chat_id     = $chat['id'];
     169                $is_admin    = WVDEVEL_Settings::is_admin_telegram_id( $telegram_id );
     170
     171                if ( ! $is_admin && ! WVDEVEL_DB::is_user_validated( $telegram_id ) ) {
     172                        self::handle_unvalidated( $telegram_id, $chat_id );
     173                        return;
     174                }
     175
     176                WVDEVEL_Submissions::handle( $telegram_id, $chat_id, $message );
     177        }
     178
    145179        private static function handle_unvalidated( $telegram_id, $chat_id ) {
    146180                if ( WVDEVEL_DB::notice_already_sent( $telegram_id, $chat_id ) ) {
  • wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-settings.php

    r3687494 r3757656  
    3232                        'misrecursos_max_mb'   => 45,
    3333                        'misrecursos_dir_key'  => '',
     34                        'entregas_enabled'     => false,
     35                        'entregas_max_mb'      => 20,
     36                        'entregas_dir_key'     => '',
    3437                );
    3538        }
  • wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-telegram-api.php

    r3669621 r3757656  
    173173                return $this->request( 'getWebhookInfo' );
    174174        }
     175
     176        /**
     177         * Resolves a Telegram file_id to its temporary file_path via getFile.
     178         * Returns the file_path string or a WP_Error.
     179         */
     180        public function get_file_path( $file_id ) {
     181                $result = $this->request( 'getFile', array( 'file_id' => (string) $file_id ) );
     182                if ( is_wp_error( $result ) ) {
     183                        return $result;
     184                }
     185                if ( empty( $result['file_path'] ) ) {
     186                        return new WP_Error( 'wvdevel_no_file_path', __( 'Telegram no devolvio la ruta del archivo.', 'wvdevel-community-assistant-telegram' ) );
     187                }
     188                return (string) $result['file_path'];
     189        }
     190
     191        /**
     192         * Downloads a Telegram file (by its getFile `file_path`) to a local path.
     193         * Bots can download files up to 20 MB. Returns true or a WP_Error.
     194         */
     195        public function download_file( $file_path, $dest_path ) {
     196                if ( empty( $this->token ) ) {
     197                        return new WP_Error( 'wvdevel_no_token', __( 'El token del bot de Telegram no esta configurado.', 'wvdevel-community-assistant-telegram' ) );
     198                }
     199
     200                $url      = sprintf( 'https://api.telegram.org/file/bot%s/%s', $this->token, ltrim( (string) $file_path, '/' ) );
     201                $response = wp_remote_get(
     202                        $url,
     203                        array(
     204                                'timeout' => 120,
     205                                'headers' => array( 'Accept' => 'application/octet-stream' ),
     206                        )
     207                );
     208                if ( is_wp_error( $response ) ) {
     209                        return $response;
     210                }
     211                if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
     212                        return new WP_Error( 'wvdevel_download_failed', __( 'No se pudo descargar el archivo desde Telegram.', 'wvdevel-community-assistant-telegram' ) );
     213                }
     214                $body = wp_remote_retrieve_body( $response );
     215                if ( '' === $body ) {
     216                        return new WP_Error( 'wvdevel_download_empty', __( 'El archivo descargado llego vacio.', 'wvdevel-community-assistant-telegram' ) );
     217                }
     218
     219                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing the downloaded bytes into the plugin's own private uploads folder; WP_Filesystem would need FTP credentials for this fire-and-forget write.
     220                if ( false === file_put_contents( $dest_path, $body ) ) {
     221                        return new WP_Error( 'wvdevel_save_failed', __( 'No se pudo escribir el archivo en el servidor.', 'wvdevel-community-assistant-telegram' ) );
     222                }
     223                return true;
     224        }
    175225}
  • wvdevel-community-assistant-telegram/tags/1.3.0/readme.txt

    r3687537 r3757656  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.2.0
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    4343  web-inaccessible folder and are delivered straight to Telegram, never
    4444  through a public URL.
     45* "Entregas": validated users can send a file to the bot in a private
     46  chat and have it stored on the server, filed under their carnet and a
     47  configurable delivery (assignment/task). Admins define each delivery
     48  (slug, label, optional file-name pattern, size limit and open window)
     49  from the "Entregas" screen and review or download the received files
     50  from "Entregas recibidas". Files are kept in the same private,
     51  web-inaccessible folder style as "Mis recursos" and are never opened
     52  by the bot.
    4553* Integrates an Ollama (conversational AI) client that is already
    4654  built but disabled by default: once your Ollama server is ready,
    … …  
    193201== Changelog ==
    194202
     203= 1.3.0 =
     204* New "Entregas" feature: validated users can send a file to the bot in a
     205  private chat and it is stored on the server under their carnet and the
     206  matching delivery. Admins define deliveries (slug, label, optional file-name
     207  pattern, size limit up to Telegram's 20 MB download ceiling, open window) from
     208  a new "Entregas" screen and review/download received files from "Entregas
     209  recibidas". Files are kept in a private, web-inaccessible folder and the bot
     210  never opens or decrypts them. Adds the `wvdevel_deliveries` and
     211  `wvdevel_submissions` tables (applied automatically on update).
     212
    195213= 1.2.0 =
    196214* New optional "rules menu": with it enabled, `/reglas` answers with an
    … …  
    226244== Upgrade Notice ==
    227245
     246= 1.3.0 =
     247Adds the "Entregas" feature: the bot can now receive files from validated users
     248into per-delivery, per-carnet private storage. Safe update; new admin screens
     249and two database tables are created automatically.
     250
    228251= 1.2.0 =
    229252Adds an optional inline-keyboard "rules menu" for /reglas. Disabled by
  • wvdevel-community-assistant-telegram/tags/1.3.0/wvdevel-community-assistant-telegram.php

    r3687494 r3757656  
    33 * Plugin Name: WVDevel Community Assistant for Telegram
    44 * Description: Configurable Telegram bot for any community or group. Validates members against an encrypted database, replies in private and in groups only when mentioned, and optionally integrates AI via Ollama.
    5  * Version: 1.2.0
     5 * Version: 1.3.0
    66 * Author: wvdevel
    77 * Text Domain: wvdevel-community-assistant-telegram
    … …  
    1616}
    1717
    18 define( 'WVDEVEL_VERSION', '1.2.0' );
     18define( 'WVDEVEL_VERSION', '1.3.0' );
    1919define( 'WVDEVEL_PLUGIN_FILE', __FILE__ );
    2020define( 'WVDEVEL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    2727require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-ollama-client.php';
    2828require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-resources.php';
     29require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-submissions.php';
    2930require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-rules.php';
    3031require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-commands.php';
    … …  
    3839        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-log-list-table.php';
    3940        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-resource-list-table.php';
     41        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-submission-list-table.php';
    4042        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-admin.php';
    4143}
  • wvdevel-community-assistant-telegram/trunk/admin/class-wvdevel-admin.php

    r3687494 r3757656  
    2020                add_action( 'admin_post_wvdevel_upload_resource', array( __CLASS__, 'upload_resource' ) );
    2121                add_action( 'admin_post_wvdevel_delete_resource', array( __CLASS__, 'delete_resource' ) );
     22                add_action( 'admin_post_wvdevel_save_entregas', array( __CLASS__, 'save_entregas' ) );
     23                add_action( 'admin_post_wvdevel_save_delivery', array( __CLASS__, 'save_delivery' ) );
     24                add_action( 'admin_post_wvdevel_delete_delivery', array( __CLASS__, 'delete_delivery' ) );
     25                add_action( 'admin_post_wvdevel_toggle_delivery', array( __CLASS__, 'toggle_delivery' ) );
     26                add_action( 'admin_post_wvdevel_download_submission', array( __CLASS__, 'download_submission' ) );
     27                add_action( 'admin_post_wvdevel_delete_submission', array( __CLASS__, 'delete_submission' ) );
    2228
    2329                add_action( 'wp_ajax_wvdevel_setup_webhook', array( __CLASS__, 'ajax_setup_webhook' ) );
    … …  
    4349                add_submenu_page( 'wvdevel-settings', __( 'Difusion (Broadcast)', 'wvdevel-community-assistant-telegram' ), __( 'Difusion (Broadcast)', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-broadcast', array( __CLASS__, 'render_broadcast_page' ) );
    4450                add_submenu_page( 'wvdevel-settings', __( 'Recursos por usuario', 'wvdevel-community-assistant-telegram' ), __( 'Recursos por usuario', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-resources', array( __CLASS__, 'render_resources_page' ) );
     51                add_submenu_page( 'wvdevel-settings', __( 'Entregas', 'wvdevel-community-assistant-telegram' ), __( 'Entregas', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-deliveries', array( __CLASS__, 'render_deliveries_page' ) );
     52                add_submenu_page( 'wvdevel-settings', __( 'Entregas recibidas', 'wvdevel-community-assistant-telegram' ), __( 'Entregas recibidas', 'wvdevel-community-assistant-telegram' ), 'manage_options', 'wvdevel-submissions', array( __CLASS__, 'render_submissions_page' ) );
    4553        }
    4654
    … …  
    110118                }
    111119                require WVDEVEL_PLUGIN_DIR . 'admin/views/resources-page.php';
     120        }
     121
     122        public static function render_deliveries_page() {
     123                if ( ! current_user_can( 'manage_options' ) ) {
     124                        return;
     125                }
     126                require WVDEVEL_PLUGIN_DIR . 'admin/views/deliveries-page.php';
     127        }
     128
     129        public static function render_submissions_page() {
     130                if ( ! current_user_can( 'manage_options' ) ) {
     131                        return;
     132                }
     133                require WVDEVEL_PLUGIN_DIR . 'admin/views/submissions-page.php';
    112134        }
    113135
    … …  
    498520                exit;
    499521        }
     522
     523        public static function save_entregas() {
     524                if ( ! current_user_can( 'manage_options' ) ) {
     525                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     526                }
     527                check_admin_referer( 'wvdevel_save_entregas' );
     528
     529                WVDEVEL_Settings::update(
     530                        array(
     531                                'entregas_enabled' => ! empty( $_POST['entregas_enabled'] ),
     532                                'entregas_max_mb'  => max( 1, min( 20, absint( wp_unslash( $_POST['entregas_max_mb'] ?? 20 ) ) ) ),
     533                        )
     534                );
     535                if ( ! empty( $_POST['entregas_enabled'] ) ) {
     536                        WVDEVEL_Submissions::ensure_protected_dir();
     537                }
     538
     539                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'saved' ), admin_url( 'admin.php' ) ) );
     540                exit;
     541        }
     542
     543        public static function save_delivery() {
     544                if ( ! current_user_can( 'manage_options' ) ) {
     545                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     546                }
     547                check_admin_referer( 'wvdevel_save_delivery' );
     548
     549                $id      = absint( wp_unslash( $_POST['delivery_id'] ?? 0 ) );
     550                $slug    = WVDEVEL_Submissions::sanitize_slug( wp_unslash( $_POST['slug'] ?? '' ) );
     551                $label   = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) );
     552                $pattern = sanitize_text_field( wp_unslash( $_POST['pattern'] ?? '' ) );
     553                $max_mb  = max( 1, min( 20, absint( wp_unslash( $_POST['max_mb'] ?? 20 ) ) ) );
     554                $prefix  = ! empty( $_POST['require_carnet_prefix'] ) ? 1 : 0;
     555                $is_open = ! empty( $_POST['is_open'] ) ? 1 : 0;
     556                $opens   = self::clean_datetime( wp_unslash( $_POST['opens_at'] ?? '' ) );
     557                $closes  = self::clean_datetime( wp_unslash( $_POST['closes_at'] ?? '' ) );
     558
     559                if ( '' === $slug || '' === $label ) {
     560                        wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_error' ), admin_url( 'admin.php' ) ) );
     561                        exit;
     562                }
     563
     564                $data = array(
     565                        'slug'                  => $slug,
     566                        'label'                 => $label,
     567                        'pattern'               => $pattern,
     568                        'max_mb'                => $max_mb,
     569                        'require_carnet_prefix' => $prefix,
     570                        'is_open'               => $is_open,
     571                        'opens_at'              => $opens,
     572                        'closes_at'             => $closes,
     573                );
     574
     575                if ( $id ) {
     576                        WVDEVEL_DB::update_delivery( $id, $data );
     577                } else {
     578                        $existing = WVDEVEL_DB::get_delivery_by_slug( $slug );
     579                        if ( $existing ) {
     580                                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_dup' ), admin_url( 'admin.php' ) ) );
     581                                exit;
     582                        }
     583                        WVDEVEL_DB::insert_delivery( $data );
     584                }
     585
     586                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'saved' ), admin_url( 'admin.php' ) ) );
     587                exit;
     588        }
     589
     590        /** Returns a Y-m-d H:i:s string or null for an empty/invalid datetime-local value. */
     591        private static function clean_datetime( $raw ) {
     592                $raw = trim( (string) $raw );
     593                if ( '' === $raw ) {
     594                        return null;
     595                }
     596                $ts = strtotime( $raw );
     597                return false === $ts ? null : gmdate( 'Y-m-d H:i:s', $ts );
     598        }
     599
     600        public static function delete_delivery() {
     601                if ( ! current_user_can( 'manage_options' ) ) {
     602                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     603                }
     604                check_admin_referer( 'wvdevel_delete_delivery' );
     605
     606                $id = absint( wp_unslash( $_GET['delivery_id'] ?? 0 ) );
     607                if ( $id ) {
     608                        WVDEVEL_DB::delete_delivery( $id );
     609                }
     610
     611                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'delivery_deleted' ), admin_url( 'admin.php' ) ) );
     612                exit;
     613        }
     614
     615        public static function toggle_delivery() {
     616                if ( ! current_user_can( 'manage_options' ) ) {
     617                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     618                }
     619                check_admin_referer( 'wvdevel_toggle_delivery' );
     620
     621                $id   = absint( wp_unslash( $_GET['delivery_id'] ?? 0 ) );
     622                $open = ! empty( $_GET['open'] ) ? 1 : 0;
     623                if ( $id ) {
     624                        WVDEVEL_DB::update_delivery( $id, array( 'is_open' => $open ) );
     625                }
     626
     627                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-deliveries', 'wvdevel_notice' => 'updated' ), admin_url( 'admin.php' ) ) );
     628                exit;
     629        }
     630
     631        public static function delete_submission() {
     632                if ( ! current_user_can( 'manage_options' ) ) {
     633                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     634                }
     635                check_admin_referer( 'wvdevel_delete_submission' );
     636
     637                $id = absint( wp_unslash( $_GET['submission_id'] ?? 0 ) );
     638                if ( $id ) {
     639                        WVDEVEL_Submissions::delete( $id );
     640                }
     641
     642                wp_safe_redirect( add_query_arg( array( 'page' => 'wvdevel-submissions', 'wvdevel_notice' => 'submission_deleted' ), admin_url( 'admin.php' ) ) );
     643                exit;
     644        }
     645
     646        /** Streams a received submission to the admin (files are not web-accessible). */
     647        public static function download_submission() {
     648                if ( ! current_user_can( 'manage_options' ) ) {
     649                        wp_die( esc_html__( 'No autorizado.', 'wvdevel-community-assistant-telegram' ) );
     650                }
     651                check_admin_referer( 'wvdevel_download_submission' );
     652
     653                $id  = absint( wp_unslash( $_GET['submission_id'] ?? 0 ) );
     654                $row = $id ? WVDEVEL_DB::get_submission( $id ) : null;
     655                if ( ! $row ) {
     656                        wp_die( esc_html__( 'Entrega no encontrada.', 'wvdevel-community-assistant-telegram' ) );
     657                }
     658                $path = WVDEVEL_Submissions::file_path( $row );
     659                if ( '' === $path || ! is_readable( $path ) ) {
     660                        wp_die( esc_html__( 'No se encontro el archivo en el servidor.', 'wvdevel-community-assistant-telegram' ) );
     661                }
     662
     663                $name = sanitize_file_name( $row->original_name );
     664                if ( '' === $name ) {
     665                        $name = 'entrega';
     666                }
     667
     668                nocache_headers();
     669                header( 'Content-Type: application/octet-stream' );
     670                header( 'Content-Disposition: attachment; filename="' . $name . '"' );
     671                header( 'Content-Length: ' . (int) filesize( $path ) );
     672
     673                while ( ob_get_level() ) {
     674                        ob_end_clean();
     675                }
     676                // phpcs:ignore WordPress.WP.AlternativeFunctions -- streaming a private, access-guarded plugin file to a capability-checked, nonce-verified admin; WordPress has no API for a raw file download.
     677                readfile( $path );
     678                exit;
     679        }
     680
    500681}
  • wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-activator.php

    r3669621 r3757656  
    3737                $log_table       = $wpdb->prefix . 'wvdevel_message_log';
    3838                $resources_table = $wpdb->prefix . 'wvdevel_resources';
     39                $deliveries_table  = $wpdb->prefix . 'wvdevel_deliveries';
     40                $submissions_table = $wpdb->prefix . 'wvdevel_submissions';
    3941
    4042                $sql = "CREATE TABLE {$users_table} (
    … …  
    8688                        PRIMARY KEY  (id),
    8789                        KEY carnet (carnet)
     90                ) {$charset_collate};
     91
     92                CREATE TABLE {$deliveries_table} (
     93                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     94                        slug VARCHAR(100) NOT NULL,
     95                        label VARCHAR(191) NOT NULL,
     96                        pattern VARCHAR(255) NULL,
     97                        max_mb SMALLINT UNSIGNED NOT NULL DEFAULT 20,
     98                        require_carnet_prefix TINYINT(1) NOT NULL DEFAULT 0,
     99                        is_open TINYINT(1) NOT NULL DEFAULT 1,
     100                        opens_at DATETIME NULL,
     101                        closes_at DATETIME NULL,
     102                        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     103                        PRIMARY KEY  (id),
     104                        UNIQUE KEY slug (slug)
     105                ) {$charset_collate};
     106
     107                CREATE TABLE {$submissions_table} (
     108                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     109                        delivery_slug VARCHAR(100) NOT NULL,
     110                        carnet VARCHAR(191) NOT NULL,
     111                        telegram_id BIGINT NOT NULL,
     112                        stored_name VARCHAR(255) NOT NULL,
     113                        original_name VARCHAR(255) NOT NULL,
     114                        mime VARCHAR(100) NULL,
     115                        size BIGINT UNSIGNED NOT NULL DEFAULT 0,
     116                        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     117                        PRIMARY KEY  (id),
     118                        KEY delivery_slug (delivery_slug),
     119                        KEY carnet (carnet)
    88120                ) {$charset_collate};";
    89121
    … …  
    102134
    103135                WVDEVEL_Resources::ensure_protected_dir();
     136                WVDEVEL_Submissions::ensure_protected_dir();
    104137        }
    105138}
  • wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-db.php

    r3669621 r3757656  
    383383                // phpcs:enable
    384384        }
     385
     386        // ---------------------------------------------------------------------
     387        // Entregas (deliveries) and their submissions.
     388        // ---------------------------------------------------------------------
     389
     390        public static function table_deliveries() {
     391                global $wpdb;
     392                return $wpdb->prefix . 'wvdevel_deliveries';
     393        }
     394
     395        public static function table_submissions() {
     396                global $wpdb;
     397                return $wpdb->prefix . 'wvdevel_submissions';
     398        }
     399
     400        /** Deliveries that are open right now (is_open plus the opens/closes window). */
     401        public static function get_open_deliveries() {
     402                global $wpdb;
     403                $table = self::table_deliveries();
     404                $now   = current_time( 'mysql' );
     405                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name, not user input; values parameterized via prepare().
     406                return $wpdb->get_results(
     407                        $wpdb->prepare(
     408                                "SELECT * FROM {$table} WHERE is_open = 1 AND ( opens_at IS NULL OR opens_at <= %s ) AND ( closes_at IS NULL OR closes_at >= %s ) ORDER BY label ASC",
     409                                $now,
     410                                $now
     411                        )
     412                );
     413        }
     414
     415        public static function get_all_deliveries() {
     416                global $wpdb;
     417                $table = self::table_deliveries();
     418                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; static admin list.
     419                return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY is_open DESC, label ASC" );
     420        }
     421
     422        public static function get_delivery( $id ) {
     423                global $wpdb;
     424                $table = self::table_deliveries();
     425                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     426                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
     427        }
     428
     429        public static function get_delivery_by_slug( $slug ) {
     430                global $wpdb;
     431                $table = self::table_deliveries();
     432                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     433                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE slug = %s", $slug ) );
     434        }
     435
     436        /** Inserts a delivery; returns the new id or 0 (e.g. duplicate slug). */
     437        public static function insert_delivery( array $data ) {
     438                global $wpdb;
     439                $table = self::table_deliveries();
     440                $data  = wp_parse_args(
     441                        $data,
     442                        array(
     443                                'slug'                  => '',
     444                                'label'                 => '',
     445                                'pattern'               => '',
     446                                'max_mb'                => 20,
     447                                'require_carnet_prefix' => 0,
     448                                'is_open'               => 1,
     449                                'opens_at'              => null,
     450                                'closes_at'             => null,
     451                                'created_at'            => current_time( 'mysql' ),
     452                        )
     453                );
     454                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->insert().
     455                $result = $wpdb->insert( $table, $data );
     456                return false !== $result ? (int) $wpdb->insert_id : 0;
     457        }
     458
     459        public static function update_delivery( $id, array $data ) {
     460                global $wpdb;
     461                $table = self::table_deliveries();
     462                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->update().
     463                return false !== $wpdb->update( $table, $data, array( 'id' => (int) $id ) );
     464        }
     465
     466        public static function delete_delivery( $id ) {
     467                global $wpdb;
     468                $table = self::table_deliveries();
     469                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->delete().
     470                return false !== $wpdb->delete( $table, array( 'id' => (int) $id ) );
     471        }
     472
     473        public static function add_submission( $delivery_slug, $carnet, $telegram_id, $stored_name, $original_name, $mime, $size ) {
     474                global $wpdb;
     475                $table = self::table_submissions();
     476                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->insert().
     477                $result = $wpdb->insert(
     478                        $table,
     479                        array(
     480                                'delivery_slug' => $delivery_slug,
     481                                'carnet'        => $carnet,
     482                                'telegram_id'   => (int) $telegram_id,
     483                                'stored_name'   => $stored_name,
     484                                'original_name' => $original_name,
     485                                'mime'          => $mime,
     486                                'size'          => (int) $size,
     487                                'created_at'    => current_time( 'mysql' ),
     488                        )
     489                );
     490                return false !== $result ? (int) $wpdb->insert_id : 0;
     491        }
     492
     493        public static function get_submission( $id ) {
     494                global $wpdb;
     495                $table = self::table_submissions();
     496                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; value parameterized via prepare().
     497                return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
     498        }
     499
     500        public static function delete_submission_row( $id ) {
     501                global $wpdb;
     502                $table = self::table_submissions();
     503                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, values escaped internally by $wpdb->delete().
     504                return false !== $wpdb->delete( $table, array( 'id' => (int) $id ) );
     505        }
     506
     507        public static function find_submission( $delivery_slug, $carnet, $original_name ) {
     508                global $wpdb;
     509                $table = self::table_submissions();
     510                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     511                return $wpdb->get_row(
     512                        $wpdb->prepare( "SELECT * FROM {$table} WHERE delivery_slug = %s AND carnet = %s AND original_name = %s", $delivery_slug, $carnet, $original_name )
     513                );
     514        }
     515
     516        public static function count_submissions_for( $delivery_slug, $carnet ) {
     517                global $wpdb;
     518                $table = self::table_submissions();
     519                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     520                return (int) $wpdb->get_var(
     521                        $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE delivery_slug = %s AND carnet = %s", $delivery_slug, $carnet )
     522                );
     523        }
     524
     525        public static function get_submissions( array $args = array() ) {
     526                global $wpdb;
     527                $table    = self::table_submissions();
     528                $per_page = isset( $args['per_page'] ) ? (int) $args['per_page'] : 30;
     529                $page     = isset( $args['page'] ) ? max( 1, (int) $args['page'] ) : 1;
     530                $offset   = ( $page - 1 ) * $per_page;
     531                $slug     = isset( $args['delivery_slug'] ) ? trim( (string) $args['delivery_slug'] ) : '';
     532                $search   = isset( $args['search'] ) ? trim( (string) $args['search'] ) : '';
     533
     534                $where  = '1=1';
     535                $params = array();
     536                if ( '' !== $slug ) {
     537                        $where   .= ' AND delivery_slug = %s';
     538                        $params[] = $slug;
     539                }
     540                if ( '' !== $search ) {
     541                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     542                        $where   .= ' AND ( carnet LIKE %s OR original_name LIKE %s )';
     543                        $params[] = $like;
     544                        $params[] = $like;
     545                }
     546                $params[] = $per_page;
     547                $params[] = $offset;
     548
     549                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; all values parameterized via prepare().
     550                return $wpdb->get_results(
     551                        $wpdb->prepare( "SELECT * FROM {$table} WHERE {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d", $params )
     552                );
     553        }
     554
     555        public static function count_submissions( array $args = array() ) {
     556                global $wpdb;
     557                $table  = self::table_submissions();
     558                $slug   = isset( $args['delivery_slug'] ) ? trim( (string) $args['delivery_slug'] ) : '';
     559                $search = isset( $args['search'] ) ? trim( (string) $args['search'] ) : '';
     560
     561                $where  = '1=1';
     562                $params = array();
     563                if ( '' !== $slug ) {
     564                        $where   .= ' AND delivery_slug = %s';
     565                        $params[] = $slug;
     566                }
     567                if ( '' !== $search ) {
     568                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     569                        $where   .= ' AND ( carnet LIKE %s OR original_name LIKE %s )';
     570                        $params[] = $like;
     571                        $params[] = $like;
     572                }
     573
     574                if ( empty( $params ) ) {
     575                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; no user input in this branch.
     576                        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE {$where}" );
     577                }
     578                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- hardcoded internal table name; values parameterized via prepare().
     579                return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$where}", $params ) );
     580        }
     581
    385582}
  • wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-message-router.php

    r3687494 r3757656  
    1616                }
    1717
     18                if ( ! empty( $update['message']['document'] ) ) {
     19                        self::handle_document( $update['message'] );
     20                        return;
     21                }
     22
    1823                if ( empty( $update['message'] ) || empty( $update['message']['text'] ) ) {
    19                         return; // Only plain text messages and callback queries are handled in this version.
     24                        return; // Only plain text messages, documents and callback queries are handled.
    2025                }
    2126
    … …  
    143148        }
    144149
     150        /**
     151         * Incoming file (document) from a user. Deliveries are accepted only in a
     152         * private chat with the bot, from validated users (admins bypass the gate).
     153         */
     154        private static function handle_document( array $message ) {
     155                $from = isset( $message['from'] ) ? $message['from'] : array();
     156                $chat = isset( $message['chat'] ) ? $message['chat'] : array();
     157                if ( empty( $from ) || ! empty( $from['is_bot'] ) || empty( $chat['id'] ) ) {
     158                        return;
     159                }
     160                if ( ! isset( $chat['type'] ) || 'private' !== $chat['type'] ) {
     161                        return; // Files are only accepted privately, never in groups.
     162                }
     163                if ( ! WVDEVEL_Submissions::is_enabled() ) {
     164                        return;
     165                }
     166
     167                $telegram_id = $from['id'];
     168                $chat_id     = $chat['id'];
     169                $is_admin    = WVDEVEL_Settings::is_admin_telegram_id( $telegram_id );
     170
     171                if ( ! $is_admin && ! WVDEVEL_DB::is_user_validated( $telegram_id ) ) {
     172                        self::handle_unvalidated( $telegram_id, $chat_id );
     173                        return;
     174                }
     175
     176                WVDEVEL_Submissions::handle( $telegram_id, $chat_id, $message );
     177        }
     178
    145179        private static function handle_unvalidated( $telegram_id, $chat_id ) {
    146180                if ( WVDEVEL_DB::notice_already_sent( $telegram_id, $chat_id ) ) {
  • wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-settings.php

    r3687494 r3757656  
    3232                        'misrecursos_max_mb'   => 45,
    3333                        'misrecursos_dir_key'  => '',
     34                        'entregas_enabled'     => false,
     35                        'entregas_max_mb'      => 20,
     36                        'entregas_dir_key'     => '',
    3437                );
    3538        }
  • wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-telegram-api.php

    r3669621 r3757656  
    173173                return $this->request( 'getWebhookInfo' );
    174174        }
     175
     176        /**
     177         * Resolves a Telegram file_id to its temporary file_path via getFile.
     178         * Returns the file_path string or a WP_Error.
     179         */
     180        public function get_file_path( $file_id ) {
     181                $result = $this->request( 'getFile', array( 'file_id' => (string) $file_id ) );
     182                if ( is_wp_error( $result ) ) {
     183                        return $result;
     184                }
     185                if ( empty( $result['file_path'] ) ) {
     186                        return new WP_Error( 'wvdevel_no_file_path', __( 'Telegram no devolvio la ruta del archivo.', 'wvdevel-community-assistant-telegram' ) );
     187                }
     188                return (string) $result['file_path'];
     189        }
     190
     191        /**
     192         * Downloads a Telegram file (by its getFile `file_path`) to a local path.
     193         * Bots can download files up to 20 MB. Returns true or a WP_Error.
     194         */
     195        public function download_file( $file_path, $dest_path ) {
     196                if ( empty( $this->token ) ) {
     197                        return new WP_Error( 'wvdevel_no_token', __( 'El token del bot de Telegram no esta configurado.', 'wvdevel-community-assistant-telegram' ) );
     198                }
     199
     200                $url      = sprintf( 'https://api.telegram.org/file/bot%s/%s', $this->token, ltrim( (string) $file_path, '/' ) );
     201                $response = wp_remote_get(
     202                        $url,
     203                        array(
     204                                'timeout' => 120,
     205                                'headers' => array( 'Accept' => 'application/octet-stream' ),
     206                        )
     207                );
     208                if ( is_wp_error( $response ) ) {
     209                        return $response;
     210                }
     211                if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
     212                        return new WP_Error( 'wvdevel_download_failed', __( 'No se pudo descargar el archivo desde Telegram.', 'wvdevel-community-assistant-telegram' ) );
     213                }
     214                $body = wp_remote_retrieve_body( $response );
     215                if ( '' === $body ) {
     216                        return new WP_Error( 'wvdevel_download_empty', __( 'El archivo descargado llego vacio.', 'wvdevel-community-assistant-telegram' ) );
     217                }
     218
     219                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing the downloaded bytes into the plugin's own private uploads folder; WP_Filesystem would need FTP credentials for this fire-and-forget write.
     220                if ( false === file_put_contents( $dest_path, $body ) ) {
     221                        return new WP_Error( 'wvdevel_save_failed', __( 'No se pudo escribir el archivo en el servidor.', 'wvdevel-community-assistant-telegram' ) );
     222                }
     223                return true;
     224        }
    175225}
  • wvdevel-community-assistant-telegram/trunk/readme.txt

    r3687537 r3757656  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.2.0
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    4343  web-inaccessible folder and are delivered straight to Telegram, never
    4444  through a public URL.
     45* "Entregas": validated users can send a file to the bot in a private
     46  chat and have it stored on the server, filed under their carnet and a
     47  configurable delivery (assignment/task). Admins define each delivery
     48  (slug, label, optional file-name pattern, size limit and open window)
     49  from the "Entregas" screen and review or download the received files
     50  from "Entregas recibidas". Files are kept in the same private,
     51  web-inaccessible folder style as "Mis recursos" and are never opened
     52  by the bot.
    4553* Integrates an Ollama (conversational AI) client that is already
    4654  built but disabled by default: once your Ollama server is ready,
    … …  
    193201== Changelog ==
    194202
     203= 1.3.0 =
     204* New "Entregas" feature: validated users can send a file to the bot in a
     205  private chat and it is stored on the server under their carnet and the
     206  matching delivery. Admins define deliveries (slug, label, optional file-name
     207  pattern, size limit up to Telegram's 20 MB download ceiling, open window) from
     208  a new "Entregas" screen and review/download received files from "Entregas
     209  recibidas". Files are kept in a private, web-inaccessible folder and the bot
     210  never opens or decrypts them. Adds the `wvdevel_deliveries` and
     211  `wvdevel_submissions` tables (applied automatically on update).
     212
    195213= 1.2.0 =
    196214* New optional "rules menu": with it enabled, `/reglas` answers with an
    … …  
    226244== Upgrade Notice ==
    227245
     246= 1.3.0 =
     247Adds the "Entregas" feature: the bot can now receive files from validated users
     248into per-delivery, per-carnet private storage. Safe update; new admin screens
     249and two database tables are created automatically.
     250
    228251= 1.2.0 =
    229252Adds an optional inline-keyboard "rules menu" for /reglas. Disabled by
  • wvdevel-community-assistant-telegram/trunk/wvdevel-community-assistant-telegram.php

    r3687494 r3757656  
    33 * Plugin Name: WVDevel Community Assistant for Telegram
    44 * Description: Configurable Telegram bot for any community or group. Validates members against an encrypted database, replies in private and in groups only when mentioned, and optionally integrates AI via Ollama.
    5  * Version: 1.2.0
     5 * Version: 1.3.0
    66 * Author: wvdevel
    77 * Text Domain: wvdevel-community-assistant-telegram
    … …  
    1616}
    1717
    18 define( 'WVDEVEL_VERSION', '1.2.0' );
     18define( 'WVDEVEL_VERSION', '1.3.0' );
    1919define( 'WVDEVEL_PLUGIN_FILE', __FILE__ );
    2020define( 'WVDEVEL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    2727require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-ollama-client.php';
    2828require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-resources.php';
     29require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-submissions.php';
    2930require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-rules.php';
    3031require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-commands.php';
    … …  
    3839        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-log-list-table.php';
    3940        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-resource-list-table.php';
     41        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-submission-list-table.php';
    4042        require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-admin.php';
    4143}
Note: See TracChangeset for help on using the changeset viewer.