Changeset 3771693 for joinotify/trunk/admin/src/Core/Workflow_Processor.php
- Timestamp:
- 10/28/2026 09:22:17 PM (7 days ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
joinotify/trunk/admin/src/Core/Workflow_Processor.php
r3692880 r3771693 1600 1600 * The builder stores one entry per template variable, each carrying where it 1601 1601 * 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. 1605 1640 * 1606 1641 * Header and body parameters of a template created with named variables … … 1609 1644 * without it, as do button parameters. 1610 1645 * 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. 1620 1667 * @return array 1621 1668 */ 1622 public static function build_template_components( $variables, $payload, $mode = 'production') {1669 public static function components_from_values( $variables ) { 1623 1670 if ( ! is_array( $variables ) || empty( $variables ) ) { 1624 1671 return array(); 1625 1672 } 1626 1673 1627 $grouped = array(); 1674 $top_level = array(); 1675 $cards = array(); 1628 1676 1629 1677 foreach ( $variables as $variable ) { … … 1632 1680 } 1633 1681 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 ) { 1634 1735 $component = strtolower( (string) ( $variable['component'] ?? 'body' ) ); 1635 1736 1636 if ( ! in_array( $component, array( 'header', 'body', 'button' ), true ) ) {1737 if ( ! in_array( $component, array( 'header', 'body', 'button', 'limited_time_offer' ), true ) ) { 1637 1738 continue; 1638 1739 } 1639 1740 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'] ?? '' ) : ''; 1644 1742 1645 1743 // Buttons are addressed one by one, so they cannot share a bucket. … … 1660 1758 } 1661 1759 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 1662 1850 $parameter = array( 1663 1851 'type' => 'text', … … 1677 1865 } 1678 1866 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; 1680 1939 } 1681 1940
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)