| @@ -94,10 +94,42 @@ | ||
| 94 | 94 | * URL). The mismatch between this constant's name and its value is |
| 95 | 95 | * deliberate — it is NOT a half-finished rename. |
| 96 | 96 | */ |
| 97 | 97 | const OPENSTATION_AGENT_CREATED_BY_META = '_desktop_mode_agent_created_by'; |
| 98 | +/** | |
| 99 | + * The VALUE keeps its pre-rebrand spelling on purpose: it is a | |
| 100 | + * persisted or externally-visible identifier, so renaming it would | |
| 101 | + * orphan data already written by live installs (or break a live | |
| 102 | + * URL). The mismatch between this constant's name and its value is | |
| 103 | + * deliberate — it is NOT a half-finished rename. | |
| 104 | + */ | |
| 105 | +const OPENSTATION_AGENT_VIBES_META = '_desktop_mode_agent_vibes'; | |
| 98 | 106 | |
| 99 | 107 | /** |
| 108 | + * Longest voice line an agent may carry. | |
| 109 | + * | |
| 110 | + * Short enough that it stays a voice rather than becoming a second | |
| 111 | + * instruction block by volume. | |
| 112 | + */ | |
| 113 | +const OPENSTATION_AGENT_VIBES_MAX_LENGTH = 120; | |
| 114 | +/** | |
| 115 | + * The VALUE keeps its pre-rebrand spelling on purpose: it is a | |
| 116 | + * persisted or externally-visible identifier, so renaming it would | |
| 117 | + * orphan data already written by live installs (or break a live | |
| 118 | + * URL). The mismatch between this constant's name and its value is | |
| 119 | + * deliberate — it is NOT a half-finished rename. | |
| 120 | + */ | |
| 121 | +const OPENSTATION_AGENT_FACE_META = '_desktop_mode_agent_face'; | |
| 122 | +/** | |
| 123 | + * The VALUE keeps its pre-rebrand spelling on purpose: it is a | |
| 124 | + * persisted or externally-visible identifier, so renaming it would | |
| 125 | + * orphan data already written by live installs (or break a live | |
| 126 | + * URL). The mismatch between this constant's name and its value is | |
| 127 | + * deliberate — it is NOT a half-finished rename. | |
| 128 | + */ | |
| 129 | +const OPENSTATION_AGENT_FACE_SEED_META = '_desktop_mode_agent_face_seed'; | |
| 130 | + | |
| 131 | +/** | |
| 100 | 132 | * Every meta key the store writes — the privacy eraser and any future |
| 101 | 133 | * cleanup path iterate this list instead of re-typing the constants. |
| 102 | 134 | * |
| 103 | 135 | * @return string[] |
| @@ -111,8 +143,11 @@ | ||
| 111 | 143 | OPENSTATION_AGENT_TRIGGERS_META, |
| 112 | 144 | OPENSTATION_AGENT_MODEL_META, |
| 113 | 145 | OPENSTATION_AGENT_RATE_LIMIT_META, |
| 114 | 146 | OPENSTATION_AGENT_CREATED_BY_META, |
| 147 | + OPENSTATION_AGENT_VIBES_META, | |
| 148 | + OPENSTATION_AGENT_FACE_META, | |
| 149 | + OPENSTATION_AGENT_FACE_SEED_META, | |
| 115 | 150 | ); |
| 116 | 151 | } |
| 117 | 152 | |
| 118 | 153 | /** |
| @@ -200,8 +235,44 @@ | ||
| 200 | 235 | 'sanitize_callback' => 'absint', |
| 201 | 236 | 'auth_callback' => $auth, |
| 202 | 237 | ) |
| 203 | 238 | ); |
| 239 | + register_meta( | |
| 240 | + 'user', | |
| 241 | + OPENSTATION_AGENT_VIBES_META, | |
| 242 | + array( | |
| 243 | + 'type' => 'string', | |
| 244 | + 'single' => true, | |
| 245 | + 'default' => '', | |
| 246 | + 'show_in_rest' => false, | |
| 247 | + 'sanitize_callback' => 'openstation_agent_sanitize_vibes', | |
| 248 | + 'auth_callback' => $auth, | |
| 249 | + ) | |
| 250 | + ); | |
| 251 | + register_meta( | |
| 252 | + 'user', | |
| 253 | + OPENSTATION_AGENT_FACE_META, | |
| 254 | + array( | |
| 255 | + 'type' => 'string', | |
| 256 | + 'single' => true, | |
| 257 | + 'default' => '', | |
| 258 | + 'show_in_rest' => false, | |
| 259 | + 'sanitize_callback' => 'openstation_agent_sanitize_face_json', | |
| 260 | + 'auth_callback' => $auth, | |
| 261 | + ) | |
| 262 | + ); | |
| 263 | + register_meta( | |
| 264 | + 'user', | |
| 265 | + OPENSTATION_AGENT_FACE_SEED_META, | |
| 266 | + array( | |
| 267 | + 'type' => 'integer', | |
| 268 | + 'single' => true, | |
| 269 | + 'default' => 0, | |
| 270 | + 'show_in_rest' => false, | |
| 271 | + 'sanitize_callback' => 'absint', | |
| 272 | + 'auth_callback' => $auth, | |
| 273 | + ) | |
| 274 | + ); | |
| 204 | 275 | } |
| 205 | 276 | add_action( 'init', 'openstation_agents_register_user_meta' ); |
| 206 | 277 | |
| 207 | 278 | // --------------------------------------------------------------------------- |
| @@ -247,8 +318,113 @@ | ||
| 247 | 318 | return (string) wp_json_encode( openstation_agents_sanitize_ability_slugs( $value ) ); |
| 248 | 319 | } |
| 249 | 320 | |
| 250 | 321 | /** |
| 322 | + * Sanitize an agent's voice line. | |
| 323 | + * | |
| 324 | + * One short line of character: "blunt, precise, no sugarcoating". It | |
| 325 | + * is appended to the agent's instructions at run time, so it reaches a | |
| 326 | + * language model. | |
| 327 | + * | |
| 328 | + * **That is not a new privilege boundary.** Writing it needs | |
| 329 | + * `edit_users`, the same capability that already lets you write | |
| 330 | + * `instructions`, which is the entire system prompt. A 120-character | |
| 331 | + * tone line is strictly less reach than that, so it deliberately sits | |
| 332 | + * behind the same gate rather than a stricter one, and this note | |
| 333 | + * exists so nobody "hardens" it later into a confusing split. | |
| 334 | + * | |
| 335 | + * Two structural guards it does need. `sanitize_text_field()` strips | |
| 336 | + * line breaks, which is load-bearing: the runner marks operator turns | |
| 337 | + * in the composed prompt, and a multi-line voice line could otherwise | |
| 338 | + * fake a turn boundary. And the length cap keeps a "voice" from | |
| 339 | + * becoming a second instruction block by volume. | |
| 340 | + * | |
| 341 | + * @param mixed $value Incoming line. | |
| 342 | + * @return string | |
| 343 | + */ | |
| 344 | +function openstation_agent_sanitize_vibes( $value ) { | |
| 345 | + if ( ! is_scalar( $value ) ) { | |
| 346 | + return ''; | |
| 347 | + } | |
| 348 | + $clean = sanitize_text_field( (string) $value ); | |
| 349 | + return mb_substr( $clean, 0, OPENSTATION_AGENT_VIBES_MAX_LENGTH ); | |
| 350 | +} | |
| 351 | + | |
| 352 | +/** | |
| 353 | + * Sanitize an agent's face for storage. | |
| 354 | + * | |
| 355 | + * The narrowing itself is `openstation_mio_narrow_look()`, which the | |
| 356 | + * WP Explorer config also calls to preview the shipped cast while the | |
| 357 | + * feature flag is off. Shared on purpose: two copies of "clamp, then | |
| 358 | + * keep what was carried" is how a preview starts drawing a face the | |
| 359 | + * seeder would never store. What this adds on top is the storage | |
| 360 | + * shape — a JSON string, and an empty one when nothing was set, so an | |
| 361 | + * agent with no opinion keeps no row at all rather than an empty blob. | |
| 362 | + * | |
| 363 | + * @param mixed $value Incoming look (array or JSON string). | |
| 364 | + * @return string JSON, or an empty string when nothing was set. | |
| 365 | + */ | |
| 366 | +function openstation_agent_sanitize_face_json( $value ) { | |
| 367 | + if ( is_string( $value ) ) { | |
| 368 | + $decoded = json_decode( $value, true ); | |
| 369 | + $value = is_array( $decoded ) ? $decoded : array(); | |
| 370 | + } | |
| 371 | + $out = openstation_mio_narrow_look( $value ); | |
| 372 | + if ( empty( $out['appearance'] ) && empty( $out['physics'] ) ) { | |
| 373 | + return ''; | |
| 374 | + } | |
| 375 | + return (string) wp_json_encode( $out ); | |
| 376 | +} | |
| 377 | + | |
| 378 | +/** | |
| 379 | + * Read an agent's voice line. | |
| 380 | + * | |
| 381 | + * @param int $user_id Agent user id. | |
| 382 | + * @return string | |
| 383 | + */ | |
| 384 | +function openstation_agent_get_vibes( $user_id ) { | |
| 385 | + return (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_VIBES_META, true ); | |
| 386 | +} | |
| 387 | + | |
| 388 | +/** | |
| 389 | + * Read an agent's stored face. | |
| 390 | + * | |
| 391 | + * A partial look: only what was overridden. Empty means "no face | |
| 392 | + * chosen", which the avatar resolver reads as the shipped robot. | |
| 393 | + * | |
| 394 | + * @param int $user_id Agent user id. | |
| 395 | + * @return array { | |
| 396 | + * @type array $appearance Partial appearance overrides. | |
| 397 | + * @type array $physics Partial silhouette overrides. | |
| 398 | + * } | |
| 399 | + */ | |
| 400 | +function openstation_agent_get_face( $user_id ) { | |
| 401 | + $raw = (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_FACE_META, true ); | |
| 402 | + if ( '' === $raw ) { | |
| 403 | + return array( | |
| 404 | + 'appearance' => array(), | |
| 405 | + 'physics' => array(), | |
| 406 | + ); | |
| 407 | + } | |
| 408 | + return openstation_sanitize_mio_look( json_decode( $raw, true ) ); | |
| 409 | +} | |
| 410 | + | |
| 411 | +/** | |
| 412 | + * Read the seed an agent's face was rolled from. | |
| 413 | + * | |
| 414 | + * Kept alongside the face rather than instead of it. The face is what | |
| 415 | + * gets drawn; the seed is provenance, and it is what lets a future | |
| 416 | + * change to the randomizer's ranges re-roll every agent in one | |
| 417 | + * migration instead of stranding them on an old palette. | |
| 418 | + * | |
| 419 | + * @param int $user_id Agent user id. | |
| 420 | + * @return int Seed, or 0 when none was recorded. | |
| 421 | + */ | |
| 422 | +function openstation_agent_get_face_seed( $user_id ) { | |
| 423 | + return (int) get_user_meta( (int) $user_id, OPENSTATION_AGENT_FACE_SEED_META, true ); | |
| 424 | +} | |
| 425 | + | |
| 426 | +/** | |
| 251 | 427 | * Sanitize the triggers array. |
| 252 | 428 | * |
| 253 | 429 | * Validates each row against the kind catalogue. Drops any row that |
| 254 | 430 | * doesn't match a known kind — one bad row never rejects the whole |
| @@ -714,9 +890,12 @@ | ||
| 714 | 890 | /** |
| 715 | 891 | * The agent's trigger row for a given invocation source, if any. |
| 716 | 892 | * |
| 717 | 893 | * Source slugs on the invoke route map 1:1 onto trigger kinds |
| 718 | - * (`chat`, `drag`, `send-to`). | |
| 894 | + * (`chat`, `drag`, `send-to`). The row is context for the invocation | |
| 895 | + * filter; it does not decide which capabilities the invocation gate | |
| 896 | + * requires, which is every capability on every trigger (see | |
| 897 | + * `openstation_agent_user_can_invoke_agent()`). | |
| 719 | 898 | * |
| 720 | 899 | * @param int $agent_user_id Agent user id. |
| 721 | 900 | * @param string $source Invocation source slug. |
| 722 | 901 | * @return array|null Trigger row, or null when the agent declares none |
| @@ -732,43 +911,65 @@ | ||
| 732 | 911 | return null; |
| 733 | 912 | } |
| 734 | 913 | |
| 735 | 914 | /** |
| 736 | - * Whether the current user may invoke THIS agent through THIS source. | |
| 915 | + * Whether the current user may invoke THIS agent. | |
| 737 | 916 | * |
| 738 | 917 | * The route-level `openstation_agents_user_can_invoke()` check is |
| 739 | 918 | * site-wide — it answers "may this user invoke agents at all". This is |
| 740 | 919 | * the per-agent half: a trigger may declare a `capability` in its |
| 741 | - * config, and until it is enforced here the field is decorative. The | |
| 742 | - * Triggers pane collects it and the store persists it, so an | |
| 743 | - * administrator restricting an agent to `manage_options` has every | |
| 744 | - * reason to believe it took effect. | |
| 920 | + * config, and the Triggers pane collects it and the store persists it, | |
| 921 | + * so an administrator restricting an agent to `manage_options` has | |
| 922 | + * every reason to believe it took effect. | |
| 745 | 923 | * |
| 746 | - * An agent with no trigger for the source, or a trigger that declares | |
| 747 | - * no capability, is left to the route-level check — requiring a | |
| 924 | + * The caller must hold EVERY capability declared on ANY of the agent's | |
| 925 | + * triggers, whichever source the request names. The source is supplied | |
| 926 | + * by the client (the invoke route takes it as a request parameter), so | |
| 927 | + * it describes how the request says it arrived, not what it is allowed | |
| 928 | + * to reach: a capability scoped to one trigger kind would be satisfied | |
| 929 | + * by naming another. A capability configured on an agent is therefore | |
| 930 | + * a property of the agent. | |
| 931 | + * | |
| 932 | + * An agent whose triggers declare no capability (including one with no | |
| 933 | + * triggers at all) is left to the route-level check — requiring a | |
| 748 | 934 | * configured trigger would lock out every agent created before triggers |
| 749 | 935 | * were set up, which is all of them by default. |
| 750 | 936 | * |
| 751 | 937 | * @param int $agent_user_id Agent user id. |
| 752 | - * @param string $source Invocation source slug. | |
| 938 | + * @param string $source Invocation source slug the request names | |
| 939 | + * (`chat`, `drag`, `send-to`). Context for | |
| 940 | + * the filter only; it does not select which | |
| 941 | + * capabilities apply. | |
| 753 | 942 | * @return bool |
| 754 | 943 | */ |
| 755 | 944 | function openstation_agent_user_can_invoke_agent( $agent_user_id, $source = 'chat' ) { |
| 756 | - $trigger = openstation_agent_trigger_for_source( $agent_user_id, $source ); | |
| 757 | - $capability = ''; | |
| 758 | - if ( is_array( $trigger ) && isset( $trigger['config']['capability'] ) ) { | |
| 759 | - $capability = trim( (string) $trigger['config']['capability'] ); | |
| 945 | + $can = true; | |
| 946 | + foreach ( openstation_agent_get_triggers( (int) $agent_user_id ) as $row ) { | |
| 947 | + if ( ! isset( $row['config']['capability'] ) || ! is_scalar( $row['config']['capability'] ) ) { | |
| 948 | + continue; | |
| 949 | + } | |
| 950 | + $capability = trim( (string) $row['config']['capability'] ); | |
| 951 | + if ( '' !== $capability && ! current_user_can( $capability ) ) { | |
| 952 | + $can = false; | |
| 953 | + break; | |
| 954 | + } | |
| 760 | 955 | } |
| 761 | 956 | |
| 762 | - $can = '' === $capability || current_user_can( $capability ); | |
| 763 | - | |
| 764 | 957 | /** |
| 765 | 958 | * Filter whether the current user may invoke a specific agent. |
| 766 | 959 | * |
| 767 | - * @param bool $can Whether invocation is allowed. | |
| 960 | + * @param bool $can Whether invocation is allowed: the | |
| 961 | + * caller holds every capability | |
| 962 | + * declared on any of the agent's | |
| 963 | + * triggers. | |
| 768 | 964 | * @param int $agent_user_id Agent user id. |
| 769 | - * @param string $source Invocation source slug. | |
| 770 | - * @param array|null $trigger The matching trigger row, if any. | |
| 965 | + * @param string $source Invocation source slug the request | |
| 966 | + * names. Client-supplied on the invoke | |
| 967 | + * route, so context rather than proof | |
| 968 | + * of how the request arrived. | |
| 969 | + * @param array|null $trigger The trigger row whose kind matches | |
| 970 | + * `$source`, if any. Context only: it | |
| 971 | + * is not what decided `$can`. | |
| 771 | 972 | */ |
| 772 | 973 | return (bool) apply_filters( |
| 773 | 974 | 'openstation_agent_user_can_invoke_agent', |
| 774 | 975 | $can, |
| @@ -773,9 +974,9 @@ | ||
| 773 | 974 | 'openstation_agent_user_can_invoke_agent', |
| 774 | 975 | $can, |
| 775 | 976 | (int) $agent_user_id, |
| 776 | 977 | (string) $source, |
| 777 | - $trigger | |
| 978 | + openstation_agent_trigger_for_source( $agent_user_id, $source ) | |
| 778 | 979 | ); |
| 779 | 980 | } |
| 780 | 981 | |
| 781 | 982 | // --------------------------------------------------------------------------- |
| @@ -805,9 +1006,9 @@ | ||
| 805 | 1006 | |
| 806 | 1007 | /** |
| 807 | 1008 | * Create an agent: synthetic user row + definition meta. |
| 808 | 1009 | * |
| 809 | - * @param array{name:string, role:string, slug?:string, description?:string, instructions?:string, abilities?:array} $args Creation args. | |
| 1010 | + * @param array{name:string, role:string, slug?:string, description?:string, instructions?:string, abilities?:array, triggers?:array, vibes?:string, face?:array|string, faceSeed?:int} $args Creation args. | |
| 810 | 1011 | * @return WP_User|WP_Error |
| 811 | 1012 | */ |
| 812 | 1013 | function openstation_agent_create( $args ) { |
| 813 | 1014 | $role = isset( $args['role'] ) ? sanitize_key( (string) $args['role'] ) : ''; |
| @@ -826,8 +1027,19 @@ | ||
| 826 | 1027 | |
| 827 | 1028 | $description = isset( $args['description'] ) ? sanitize_text_field( (string) $args['description'] ) : ''; |
| 828 | 1029 | $instructions = isset( $args['instructions'] ) ? wp_kses_post( (string) $args['instructions'] ) : ''; |
| 829 | 1030 | $abilities = isset( $args['abilities'] ) ? openstation_agents_sanitize_ability_slugs( $args['abilities'] ) : array(); |
| 1031 | + $triggers = isset( $args['triggers'] ) ? openstation_agent_sanitize_triggers( $args['triggers'] ) : array(); | |
| 1032 | + $vibes = isset( $args['vibes'] ) ? openstation_agent_sanitize_vibes( $args['vibes'] ) : ''; | |
| 1033 | + $face = isset( $args['face'] ) ? openstation_agent_sanitize_face_json( $args['face'] ) : ''; | |
| 1034 | + // Every agent gets a seed even when nobody chose a face, so the | |
| 1035 | + // backfill has something deterministic to roll from and two admins | |
| 1036 | + // racing it land on the same portrait. `crc32` of the login is | |
| 1037 | + // stable, cheap, and already unique per agent. | |
| 1038 | + $seed = isset( $args['faceSeed'] ) ? absint( $args['faceSeed'] ) : 0; | |
| 1039 | + if ( 0 === $seed ) { | |
| 1040 | + $seed = crc32( (string) $user->user_login ); | |
| 1041 | + } | |
| 830 | 1042 | |
| 831 | 1043 | if ( '' !== $description ) { |
| 832 | 1044 | update_user_meta( $user->ID, OPENSTATION_AGENT_DESCRIPTION_META, $description ); |
| 833 | 1045 | } |
| @@ -836,8 +1048,18 @@ | ||
| 836 | 1048 | } |
| 837 | 1049 | if ( ! empty( $abilities ) ) { |
| 838 | 1050 | update_user_meta( $user->ID, OPENSTATION_AGENT_ABILITIES_META, wp_json_encode( $abilities ) ); |
| 839 | 1051 | } |
| 1052 | + if ( ! empty( $triggers ) ) { | |
| 1053 | + update_user_meta( $user->ID, OPENSTATION_AGENT_TRIGGERS_META, wp_json_encode( $triggers ) ); | |
| 1054 | + } | |
| 1055 | + if ( '' !== $vibes ) { | |
| 1056 | + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes ); | |
| 1057 | + } | |
| 1058 | + if ( '' !== $face ) { | |
| 1059 | + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face ); | |
| 1060 | + } | |
| 1061 | + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed ); | |
| 840 | 1062 | update_user_meta( $user->ID, OPENSTATION_AGENT_CREATED_BY_META, get_current_user_id() ); |
| 841 | 1063 | |
| 842 | 1064 | /** |
| 843 | 1065 | * Fires after an agent is created. |
| @@ -855,8 +1077,11 @@ | ||
| 855 | 1077 | 'role' => $role, |
| 856 | 1078 | 'description' => $description, |
| 857 | 1079 | 'instructions' => $instructions, |
| 858 | 1080 | 'abilities' => $abilities, |
| 1081 | + 'vibes' => $vibes, | |
| 1082 | + 'face' => $face, | |
| 1083 | + 'faceSeed' => $seed, | |
| 859 | 1084 | ), |
| 860 | 1085 | get_current_user_id() |
| 861 | 1086 | ); |
| 862 | 1087 | |
| @@ -868,9 +1093,10 @@ | ||
| 868 | 1093 | * fields, applies the valid ones, and fires `openstation_agent_updated` |
| 869 | 1094 | * once with a before/after map of everything that changed. |
| 870 | 1095 | * |
| 871 | 1096 | * Recognized fields: `name`, `role`, `description`, `instructions`, |
| 872 | - * `abilities`, `triggers`, `model`, `rateLimit`. | |
| 1097 | + * `abilities`, `triggers`, `model`, `rateLimit`, `vibes`, `face`, | |
| 1098 | + * `faceSeed`. | |
| 873 | 1099 | * |
| 874 | 1100 | * @param int $user_id Agent user id. |
| 875 | 1101 | * @param array $fields Field map. |
| 876 | 1102 | * @return true|WP_Error |
| @@ -893,9 +1119,12 @@ | ||
| 893 | 1119 | 'openstation_agent_invalid_name', |
| 894 | 1120 | __( 'Agent name cannot be empty.', 'desktop-mode' ) |
| 895 | 1121 | ); |
| 896 | 1122 | } |
| 897 | - if ( $name !== (string) $user->display_name ) { | |
| 1123 | + // Compared as plain text: the stored name carries entities | |
| 1124 | + // (`&`), and the client sends back the decoded one it was | |
| 1125 | + // given on every save. Raw, that reads as a rename. | |
| 1126 | + if ( openstation_plain_text_title( $name ) !== openstation_plain_text_title( $user->display_name ) ) { | |
| 898 | 1127 | $changed['name'] = array( |
| 899 | 1128 | 'from' => (string) $user->display_name, |
| 900 | 1129 | 'to' => $name, |
| 901 | 1130 | ); |
| @@ -1003,8 +1232,52 @@ | ||
| 1003 | 1232 | delete_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META ); |
| 1004 | 1233 | } else { |
| 1005 | 1234 | update_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META, $rate ); |
| 1006 | 1235 | } |
| 1236 | + } | |
| 1237 | + } | |
| 1238 | + | |
| 1239 | + if ( isset( $fields['vibes'] ) ) { | |
| 1240 | + $vibes = openstation_agent_sanitize_vibes( $fields['vibes'] ); | |
| 1241 | + $before = openstation_agent_get_vibes( $user->ID ); | |
| 1242 | + if ( $vibes !== $before ) { | |
| 1243 | + $changed['vibes'] = array( | |
| 1244 | + 'from' => $before, | |
| 1245 | + 'to' => $vibes, | |
| 1246 | + ); | |
| 1247 | + if ( '' === $vibes ) { | |
| 1248 | + delete_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META ); | |
| 1249 | + } else { | |
| 1250 | + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes ); | |
| 1251 | + } | |
| 1252 | + } | |
| 1253 | + } | |
| 1254 | + | |
| 1255 | + if ( isset( $fields['face'] ) ) { | |
| 1256 | + $face = openstation_agent_sanitize_face_json( $fields['face'] ); | |
| 1257 | + $before = (string) get_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, true ); | |
| 1258 | + if ( $face !== $before ) { | |
| 1259 | + $changed['face'] = array( | |
| 1260 | + 'from' => $before, | |
| 1261 | + 'to' => $face, | |
| 1262 | + ); | |
| 1263 | + if ( '' === $face ) { | |
| 1264 | + delete_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META ); | |
| 1265 | + } else { | |
| 1266 | + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face ); | |
| 1267 | + } | |
| 1268 | + } | |
| 1269 | + } | |
| 1270 | + | |
| 1271 | + if ( isset( $fields['faceSeed'] ) ) { | |
| 1272 | + $seed = absint( $fields['faceSeed'] ); | |
| 1273 | + $before = openstation_agent_get_face_seed( $user->ID ); | |
| 1274 | + if ( $seed !== $before ) { | |
| 1275 | + $changed['faceSeed'] = array( | |
| 1276 | + 'from' => $before, | |
| 1277 | + 'to' => $seed, | |
| 1278 | + ); | |
| 1279 | + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed ); | |
| 1007 | 1280 | } |
| 1008 | 1281 | } |
| 1009 | 1282 | |
| 1010 | 1283 | if ( ! empty( $changed ) ) { |