Plugin Directory


Ignore:
Timestamp:
10/28/2026 09:22:17 PM (7 days ago)
Author:
meumouse
Message:

Release 2.6.0

File:
1 edited

Legend:

Unmodified
Added
Removed
  • joinotify/trunk/admin/src/Core/Workflow_Processor.php

    r3692880 r3771693  
    16001600     * The builder stores one entry per template variable, each carrying where it
    16011601     * belongs (`component`, and `sub_type`/`index` for button variables) and the
    1602      * Joinotify placeholder that fills it. Meta wants those grouped per
    1603      * component, with lowercase types, and button parameters split by button
    1604      * index.
     1602     * Joinotify placeholder that fills it. The placeholders are resolved here;
     1603     * packing the values into Meta's dialect is components_from_values().
     1604     *
     1605     * @since 2.3.0
     1606     * @version 2.6.0
     1607     * @param array  $variables | Variable map stored on the action.
     1608     * @param array  $payload | Runtime trigger payload.
     1609     * @param string $mode | Placeholder resolution mode ('production' or 'sandbox').
     1610     * @return array
     1611     */
     1612    public static function build_template_components( $variables, $payload, $mode = 'production' ) {
     1613        if ( ! is_array( $variables ) || empty( $variables ) ) {
     1614            return array();
     1615        }
     1616
     1617        foreach ( $variables as $position => $variable ) {
     1618            if ( ! is_array( $variable ) ) {
     1619                continue;
     1620            }
     1621
     1622            $raw = (string) ( $variable['value'] ?? '' );
     1623
     1624            $variables[ $position ]['value'] = 'sandbox' === $mode
     1625                ? Placeholders::replace_placeholders( $raw, $payload, 'sandbox' )
     1626                : joinotify_prepare_message( $raw, $payload );
     1627        }
     1628
     1629        return self::components_from_values( $variables );
     1630    }
     1631
     1632
     1633    /**
     1634     * Pack variables whose values are already resolved into Meta's `components` payload.
     1635     *
     1636     * Meta wants the values grouped per component, with lowercase types, and
     1637     * button parameters split by button index. Nothing is resolved here: a value
     1638     * holding `{{ ... }}` goes out as written, which is what lets another plugin
     1639     * hand over values it resolved with its own placeholders.
    16051640     *
    16061641     * Header and body parameters of a template created with named variables
    … …  
    16091644     * without it, as do button parameters.
    16101645     *
    1611      * Every value travels as a `text` parameter, numbers included: Meta has no
    1612      * numeric text type, and a digit string is what it expects for an order
    1613      * number or a count.
    1614      *
    1615      * @since 2.3.0
    1616      * @version 2.4.2
    1617      * @param array  $variables | Variable map stored on the action.
    1618      * @param array  $payload | Runtime trigger payload.
    1619      * @param string $mode | Placeholder resolution mode ('production' or 'sandbox').
     1646     * Every text value travels as a `text` parameter, numbers included: Meta has
     1647     * no numeric text type, and a digit string is what it expects for an order
     1648     * number or a count. A header variable whose `format` is `image`, `video` or
     1649     * `document` is a media header instead: its value is the file link, or the
     1650     * id of a file already uploaded to Meta, and a document also carries the
     1651     * file name read from the link. A `copy_code` button carries its value as
     1652     * the coupon. The button of an AUTHENTICATION template (`format` `otp`)
     1653     * repeats the login code, so when it is left empty it takes the first body
     1654     * value, which is where that code lives.
     1655     *
     1656     * A location header is built from its four variables (`format` `location`,
     1657     * keyed `latitude`, `longitude`, `name`, `address`). A `limited_time_offer`
     1658     * variable holds when the offer expires — a date, a Unix timestamp in
     1659     * seconds or milliseconds, or a relative time such as `+2 days` — and goes
     1660     * out in milliseconds. Variables with a `card` of 0 or more belong to that
     1661     * carousel card: each card is packed the same way and they all go out in
     1662     * one `carousel` component, where a quick-reply button left empty sends a
     1663     * payload naming its card and button.
     1664     *
     1665     * @since 2.6.0
     1666     * @param array $variables | One entry per variable: component, sub_type, index, key, format, card and the resolved value.
    16201667     * @return array
    16211668     */
    1622     public static function build_template_components( $variables, $payload, $mode = 'production' ) {
     1669    public static function components_from_values( $variables ) {
    16231670        if ( ! is_array( $variables ) || empty( $variables ) ) {
    16241671            return array();
    16251672        }
    16261673
    1627         $grouped = array();
     1674        $top_level = array();
     1675        $cards = array();
    16281676
    16291677        foreach ( $variables as $variable ) {
    … …  
    16321680            }
    16331681
     1682            $card = (int) ( $variable['card'] ?? -1 );
     1683
     1684            if ( $card >= 0 ) {
     1685                $cards[ $card ][] = $variable;
     1686            } else {
     1687                $top_level[] = $variable;
     1688            }
     1689        }
     1690
     1691        $components = self::group_component_values( $top_level );
     1692
     1693        if ( ! empty( $cards ) ) {
     1694            ksort( $cards );
     1695            $carousel = array();
     1696
     1697            foreach ( $cards as $card_index => $card_variables ) {
     1698                $carousel[] = array(
     1699                    'card_index' => (int) $card_index,
     1700                    'components' => self::group_component_values( $card_variables, (int) $card_index ),
     1701                );
     1702            }
     1703
     1704            $components[] = array(
     1705                'type' => 'carousel',
     1706                'cards' => $carousel,
     1707            );
     1708        }
     1709
     1710        return $components;
     1711    }
     1712
     1713
     1714    /**
     1715     * Group the resolved values of one message (or one carousel card) per component.
     1716     *
     1717     * @since 2.6.0
     1718     * @param array $variables | Variables with their resolved value.
     1719     * @param int   $card | Carousel card position, -1 outside a carousel.
     1720     * @return array
     1721     */
     1722    protected static function group_component_values( $variables, $card = -1 ) {
     1723        $grouped = array();
     1724        $first_body = '';
     1725
     1726        foreach ( $variables as $variable ) {
     1727            if ( 'body' === strtolower( (string) ( $variable['component'] ?? 'body' ) ) ) {
     1728                $first_body = is_scalar( $variable['value'] ?? '' ) ? (string) ( $variable['value'] ?? '' ) : '';
     1729
     1730                break;
     1731            }
     1732        }
     1733
     1734        foreach ( $variables as $variable ) {
    16341735            $component = strtolower( (string) ( $variable['component'] ?? 'body' ) );
    16351736
    1636             if ( ! in_array( $component, array( 'header', 'body', 'button' ), true ) ) {
     1737            if ( ! in_array( $component, array( 'header', 'body', 'button', 'limited_time_offer' ), true ) ) {
    16371738                continue;
    16381739            }
    16391740
    1640             $raw = (string) ( $variable['value'] ?? '' );
    1641             $value = 'sandbox' === $mode
    1642                 ? Placeholders::replace_placeholders( $raw, $payload, 'sandbox' )
    1643                 : joinotify_prepare_message( $raw, $payload );
     1741            $value = is_scalar( $variable['value'] ?? '' ) ? (string) ( $variable['value'] ?? '' ) : '';
    16441742
    16451743            // Buttons are addressed one by one, so they cannot share a bucket.
    … …  
    16601758            }
    16611759
     1760            $format = strtolower( (string) ( $variable['format'] ?? '' ) );
     1761
     1762            if ( 'header' === $component && in_array( $format, array( 'image', 'video', 'document' ), true ) ) {
     1763                $grouped[ $bucket ]['parameters'][] = array(
     1764                    'type' => $format,
     1765                    $format => self::media_header_object( $format, $value ),
     1766                );
     1767
     1768                continue;
     1769            }
     1770
     1771            // The four location variables fill one parameter between them.
     1772            if ( 'header' === $component && 'location' === $format ) {
     1773                $field = strtolower( trim( (string) ( $variable['key'] ?? '' ) ) );
     1774
     1775                if ( ! in_array( $field, array( 'latitude', 'longitude', 'name', 'address' ), true ) ) {
     1776                    continue;
     1777                }
     1778
     1779                $position = null;
     1780
     1781                foreach ( $grouped[ $bucket ]['parameters'] as $existing => $parameter ) {
     1782                    if ( 'location' === ( $parameter['type'] ?? '' ) ) {
     1783                        $position = $existing;
     1784
     1785                        break;
     1786                    }
     1787                }
     1788
     1789                if ( null === $position ) {
     1790                    $grouped[ $bucket ]['parameters'][] = array(
     1791                        'type' => 'location',
     1792                        'location' => array(),
     1793                    );
     1794                    $position = array_key_last( $grouped[ $bucket ]['parameters'] );
     1795                }
     1796
     1797                $text = self::prepare_template_text( $value );
     1798
     1799                // A decimal comma (-23,55) is what many sites store; Meta wants a point.
     1800                if ( in_array( $field, array( 'latitude', 'longitude' ), true ) ) {
     1801                    $text = str_replace( ',', '.', $text );
     1802                }
     1803
     1804                // Name and address are optional; an empty one is left out.
     1805                if ( '' !== $text ) {
     1806                    $grouped[ $bucket ]['parameters'][ $position ]['location'][ $field ] = $text;
     1807                }
     1808
     1809                continue;
     1810            }
     1811
     1812            if ( 'limited_time_offer' === $component ) {
     1813                $expiration = self::expiration_ms( $value );
     1814
     1815                // A missing or unreadable date would only earn a refusal.
     1816                if ( null !== $expiration ) {
     1817                    $grouped[ $bucket ]['parameters'][] = array(
     1818                        'type' => 'limited_time_offer',
     1819                        'limited_time_offer' => array( 'expiration_time_ms' => $expiration ),
     1820                    );
     1821                }
     1822
     1823                continue;
     1824            }
     1825
     1826            if ( 'button' === $component && 'copy_code' === ( $grouped[ $bucket ]['sub_type'] ?? '' ) ) {
     1827                $grouped[ $bucket ]['parameters'][] = array(
     1828                    'type' => 'coupon_code',
     1829                    'coupon_code' => self::prepare_template_text( $value ),
     1830                );
     1831
     1832                continue;
     1833            }
     1834
     1835            if ( 'button' === $component && 'quick_reply' === ( $grouped[ $bucket ]['sub_type'] ?? '' ) ) {
     1836                $payload = self::prepare_template_text( $value );
     1837
     1838                $grouped[ $bucket ]['parameters'][] = array(
     1839                    'type' => 'payload',
     1840                    'payload' => '' !== $payload ? $payload : sprintf( 'card_%d_button_%d', max( 0, $card ), (int) ( $variable['index'] ?? 0 ) ),
     1841                );
     1842
     1843                continue;
     1844            }
     1845
     1846            if ( 'otp' === $format && '' === trim( $value ) ) {
     1847                $value = $first_body;
     1848            }
     1849
    16621850            $parameter = array(
    16631851                'type' => 'text',
    … …  
    16771865        }
    16781866
    1679         return array_values( $grouped );
     1867        // A component left without a parameter (an offer with no readable
     1868        // date) is dropped rather than sent empty.
     1869        return array_values( array_filter( $grouped, fn( $component ) => ! empty( $component['parameters'] ) ) );
     1870    }
     1871
     1872
     1873    /**
     1874     * Read when a limited-time offer expires, in the milliseconds Meta expects.
     1875     *
     1876     * Digits are a Unix timestamp: in milliseconds from 13 digits on, in
     1877     * seconds below. Anything else is read as a date or a relative time in the
     1878     * site's timezone.
     1879     *
     1880     * @since 2.6.0
     1881     * @param string $value | Resolved expiration.
     1882     * @return int|float|null Milliseconds since the epoch, or null when it cannot be read.
     1883     */
     1884    protected static function expiration_ms( $value ) {
     1885        $value = trim( (string) $value );
     1886
     1887        if ( '' === $value ) {
     1888            return null;
     1889        }
     1890
     1891        if ( preg_match( '/^\d+$/', $value ) ) {
     1892            $milliseconds = strlen( $value ) >= 13 ? (float) $value : (float) $value * 1000;
     1893        } else {
     1894            try {
     1895                $date = new \DateTimeImmutable( $value, wp_timezone() );
     1896            } catch ( \Exception $e ) {
     1897                return null;
     1898            }
     1899
     1900            $milliseconds = (float) $date->getTimestamp() * 1000;
     1901        }
     1902
     1903        // Milliseconds outgrow a 32-bit integer; only a 64-bit build sends an int.
     1904        return PHP_INT_SIZE >= 8 ? (int) $milliseconds : $milliseconds;
     1905    }
     1906
     1907
     1908    /**
     1909     * Describe the file of a media header the way Meta expects it.
     1910     *
     1911     * A value made of digits only is the id of a file uploaded to Meta; anything
     1912     * else is a link. A document sent by link also carries the file name, which
     1913     * is what the recipient sees on the attachment.
     1914     *
     1915     * @since 2.6.0
     1916     * @param string $format | image, video or document.
     1917     * @param string $value | File link or Meta media id.
     1918     * @return array
     1919     */
     1920    protected static function media_header_object( $format, $value ) {
     1921        $value = trim( (string) $value );
     1922
     1923        if ( preg_match( '/^\d+$/', $value ) ) {
     1924            return array( 'id' => $value );
     1925        }
     1926
     1927        $media = array( 'link' => $value );
     1928
     1929        if ( 'document' === $format ) {
     1930            $path = (string) wp_parse_url( $value, PHP_URL_PATH );
     1931            $filename = '' !== $path ? rawurldecode( basename( $path ) ) : '';
     1932
     1933            if ( '' !== $filename ) {
     1934                $media['filename'] = $filename;
     1935            }
     1936        }
     1937
     1938        return $media;
    16801939    }
    16811940
Note: See TracChangeset for help on using the changeset viewer.