Changeset 3757656
- Timestamp:
- 10/21/2026 02:45:45 AM (3 weeks ago)
- Location:
- wvdevel-community-assistant-telegram
- Files:
-
- 8 added
- 16 edited
- 1 copied
-
tags/1.3.0 (copied) (copied from wvdevel-community-assistant-telegram/trunk )
-
tags/1.3.0/admin/class-wvdevel-admin.php (modified) (4 diffs)
-
tags/1.3.0/admin/class-wvdevel-submission-list-table.php (added)
-
tags/1.3.0/admin/views/deliveries-page.php (added)
-
tags/1.3.0/admin/views/submissions-page.php (added)
-
tags/1.3.0/includes/class-wvdevel-activator.php (modified) (3 diffs)
-
tags/1.3.0/includes/class-wvdevel-db.php (modified) (1 diff)
-
tags/1.3.0/includes/class-wvdevel-message-router.php (modified) (2 diffs)
-
tags/1.3.0/includes/class-wvdevel-settings.php (modified) (1 diff)
-
tags/1.3.0/includes/class-wvdevel-submissions.php (added)
-
tags/1.3.0/includes/class-wvdevel-telegram-api.php (modified) (1 diff)
-
tags/1.3.0/readme.txt (modified) (4 diffs)
-
tags/1.3.0/wvdevel-community-assistant-telegram.php (modified) (4 diffs)
-
trunk/admin/class-wvdevel-admin.php (modified) (4 diffs)
-
trunk/admin/class-wvdevel-submission-list-table.php (added)
-
trunk/admin/views/deliveries-page.php (added)
-
trunk/admin/views/submissions-page.php (added)
-
trunk/includes/class-wvdevel-activator.php (modified) (3 diffs)
-
trunk/includes/class-wvdevel-db.php (modified) (1 diff)
-
trunk/includes/class-wvdevel-message-router.php (modified) (2 diffs)
-
trunk/includes/class-wvdevel-settings.php (modified) (1 diff)
-
trunk/includes/class-wvdevel-submissions.php (added)
-
trunk/includes/class-wvdevel-telegram-api.php (modified) (1 diff)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/wvdevel-community-assistant-telegram.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
wvdevel-community-assistant-telegram/tags/1.3.0/admin/class-wvdevel-admin.php
r3687494 r3757656 20 20 add_action( 'admin_post_wvdevel_upload_resource', array( __CLASS__, 'upload_resource' ) ); 21 21 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' ) ); 22 28 23 29 add_action( 'wp_ajax_wvdevel_setup_webhook', array( __CLASS__, 'ajax_setup_webhook' ) ); … … 43 49 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' ) ); 44 50 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' ) ); 45 53 } 46 54 … … 110 118 } 111 119 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'; 112 134 } 113 135 … … 498 520 exit; 499 521 } 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 500 681 } -
wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-activator.php
r3669621 r3757656 37 37 $log_table = $wpdb->prefix . 'wvdevel_message_log'; 38 38 $resources_table = $wpdb->prefix . 'wvdevel_resources'; 39 $deliveries_table = $wpdb->prefix . 'wvdevel_deliveries'; 40 $submissions_table = $wpdb->prefix . 'wvdevel_submissions'; 39 41 40 42 $sql = "CREATE TABLE {$users_table} ( … … 86 88 PRIMARY KEY (id), 87 89 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) 88 120 ) {$charset_collate};"; 89 121 … … 102 134 103 135 WVDEVEL_Resources::ensure_protected_dir(); 136 WVDEVEL_Submissions::ensure_protected_dir(); 104 137 } 105 138 } -
wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-db.php
r3669621 r3757656 383 383 // phpcs:enable 384 384 } 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 385 582 } -
wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-message-router.php
r3687494 r3757656 16 16 } 17 17 18 if ( ! empty( $update['message']['document'] ) ) { 19 self::handle_document( $update['message'] ); 20 return; 21 } 22 18 23 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. 20 25 } 21 26 … … 143 148 } 144 149 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 145 179 private static function handle_unvalidated( $telegram_id, $chat_id ) { 146 180 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 32 32 'misrecursos_max_mb' => 45, 33 33 'misrecursos_dir_key' => '', 34 'entregas_enabled' => false, 35 'entregas_max_mb' => 20, 36 'entregas_dir_key' => '', 34 37 ); 35 38 } -
wvdevel-community-assistant-telegram/tags/1.3.0/includes/class-wvdevel-telegram-api.php
r3669621 r3757656 173 173 return $this->request( 'getWebhookInfo' ); 174 174 } 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 } 175 225 } -
wvdevel-community-assistant-telegram/tags/1.3.0/readme.txt
r3687537 r3757656 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 43 43 web-inaccessible folder and are delivered straight to Telegram, never 44 44 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. 45 53 * Integrates an Ollama (conversational AI) client that is already 46 54 built but disabled by default: once your Ollama server is ready, … … 193 201 == Changelog == 194 202 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 195 213 = 1.2.0 = 196 214 * New optional "rules menu": with it enabled, `/reglas` answers with an … … 226 244 == Upgrade Notice == 227 245 246 = 1.3.0 = 247 Adds the "Entregas" feature: the bot can now receive files from validated users 248 into per-delivery, per-carnet private storage. Safe update; new admin screens 249 and two database tables are created automatically. 250 228 251 = 1.2.0 = 229 252 Adds 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 3 3 * Plugin Name: WVDevel Community Assistant for Telegram 4 4 * 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.05 * Version: 1.3.0 6 6 * Author: wvdevel 7 7 * Text Domain: wvdevel-community-assistant-telegram … … 16 16 } 17 17 18 define( 'WVDEVEL_VERSION', '1. 2.0' );18 define( 'WVDEVEL_VERSION', '1.3.0' ); 19 19 define( 'WVDEVEL_PLUGIN_FILE', __FILE__ ); 20 20 define( 'WVDEVEL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 27 27 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-ollama-client.php'; 28 28 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-resources.php'; 29 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-submissions.php'; 29 30 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-rules.php'; 30 31 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-commands.php'; … … 38 39 require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-log-list-table.php'; 39 40 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'; 40 42 require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-admin.php'; 41 43 } -
wvdevel-community-assistant-telegram/trunk/admin/class-wvdevel-admin.php
r3687494 r3757656 20 20 add_action( 'admin_post_wvdevel_upload_resource', array( __CLASS__, 'upload_resource' ) ); 21 21 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' ) ); 22 28 23 29 add_action( 'wp_ajax_wvdevel_setup_webhook', array( __CLASS__, 'ajax_setup_webhook' ) ); … … 43 49 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' ) ); 44 50 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' ) ); 45 53 } 46 54 … … 110 118 } 111 119 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'; 112 134 } 113 135 … … 498 520 exit; 499 521 } 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 500 681 } -
wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-activator.php
r3669621 r3757656 37 37 $log_table = $wpdb->prefix . 'wvdevel_message_log'; 38 38 $resources_table = $wpdb->prefix . 'wvdevel_resources'; 39 $deliveries_table = $wpdb->prefix . 'wvdevel_deliveries'; 40 $submissions_table = $wpdb->prefix . 'wvdevel_submissions'; 39 41 40 42 $sql = "CREATE TABLE {$users_table} ( … … 86 88 PRIMARY KEY (id), 87 89 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) 88 120 ) {$charset_collate};"; 89 121 … … 102 134 103 135 WVDEVEL_Resources::ensure_protected_dir(); 136 WVDEVEL_Submissions::ensure_protected_dir(); 104 137 } 105 138 } -
wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-db.php
r3669621 r3757656 383 383 // phpcs:enable 384 384 } 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 385 582 } -
wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-message-router.php
r3687494 r3757656 16 16 } 17 17 18 if ( ! empty( $update['message']['document'] ) ) { 19 self::handle_document( $update['message'] ); 20 return; 21 } 22 18 23 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. 20 25 } 21 26 … … 143 148 } 144 149 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 145 179 private static function handle_unvalidated( $telegram_id, $chat_id ) { 146 180 if ( WVDEVEL_DB::notice_already_sent( $telegram_id, $chat_id ) ) { -
wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-settings.php
r3687494 r3757656 32 32 'misrecursos_max_mb' => 45, 33 33 'misrecursos_dir_key' => '', 34 'entregas_enabled' => false, 35 'entregas_max_mb' => 20, 36 'entregas_dir_key' => '', 34 37 ); 35 38 } -
wvdevel-community-assistant-telegram/trunk/includes/class-wvdevel-telegram-api.php
r3669621 r3757656 173 173 return $this->request( 'getWebhookInfo' ); 174 174 } 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 } 175 225 } -
wvdevel-community-assistant-telegram/trunk/readme.txt
r3687537 r3757656 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 43 43 web-inaccessible folder and are delivered straight to Telegram, never 44 44 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. 45 53 * Integrates an Ollama (conversational AI) client that is already 46 54 built but disabled by default: once your Ollama server is ready, … … 193 201 == Changelog == 194 202 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 195 213 = 1.2.0 = 196 214 * New optional "rules menu": with it enabled, `/reglas` answers with an … … 226 244 == Upgrade Notice == 227 245 246 = 1.3.0 = 247 Adds the "Entregas" feature: the bot can now receive files from validated users 248 into per-delivery, per-carnet private storage. Safe update; new admin screens 249 and two database tables are created automatically. 250 228 251 = 1.2.0 = 229 252 Adds an optional inline-keyboard "rules menu" for /reglas. Disabled by -
wvdevel-community-assistant-telegram/trunk/wvdevel-community-assistant-telegram.php
r3687494 r3757656 3 3 * Plugin Name: WVDevel Community Assistant for Telegram 4 4 * 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.05 * Version: 1.3.0 6 6 * Author: wvdevel 7 7 * Text Domain: wvdevel-community-assistant-telegram … … 16 16 } 17 17 18 define( 'WVDEVEL_VERSION', '1. 2.0' );18 define( 'WVDEVEL_VERSION', '1.3.0' ); 19 19 define( 'WVDEVEL_PLUGIN_FILE', __FILE__ ); 20 20 define( 'WVDEVEL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 27 27 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-ollama-client.php'; 28 28 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-resources.php'; 29 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-submissions.php'; 29 30 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-rules.php'; 30 31 require_once WVDEVEL_PLUGIN_DIR . 'includes/class-wvdevel-commands.php'; … … 38 39 require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-log-list-table.php'; 39 40 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'; 40 42 require_once WVDEVEL_PLUGIN_DIR . 'admin/class-wvdevel-admin.php'; 41 43 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)