Export Personal * Data / Erase Personal Data) and the privacy-policy guide. Requests are * resolved by e-mail to a registered user of the current site. */ final class PrivacyIntegration { public function __construct(private Plugin $plugin) { } public function register(): void { add_filter('wp_privacy_personal_data_exporters', function (array $exporters): array { $exporters['aiwend'] = ['exporter_friendly_name' => __('Aiwend AI assistant', 'aiwend'), 'callback' => [$this, 'export']]; return $exporters; }); add_filter('wp_privacy_personal_data_erasers', function (array $erasers): array { $erasers['aiwend'] = ['eraser_friendly_name' => __('Aiwend AI assistant', 'aiwend'), 'callback' => [$this, 'erase']]; return $erasers; }); add_action('admin_init', [$this, 'policyText']); } /** @return array{data: list>, done: bool} */ public function export(string $email, int $page = 1): array { $user = get_user_by('email', $email); if (!$user) { return ['data' => [], 'done' => true]; } $groups = [ 'aiwend-conversations' => __('AI assistant conversations', 'aiwend'), 'aiwend-messages' => __('AI assistant messages', 'aiwend'), 'aiwend-knowledge' => __('AI assistant knowledge sources you added', 'aiwend'), ]; $result = $this->service()->export(get_current_blog_id(), (int) $user->ID, $page); $data = []; foreach ($result['items'] as $item) { $data[] = [ 'group_id' => $item['group'], 'group_label' => $groups[$item['group']] ?? $item['group'], 'item_id' => $item['id'], 'data' => array_map(static fn($name, $value) => ['name' => $name, 'value' => $value], array_keys($item['data']), array_values($item['data'])), ]; } return ['data' => $data, 'done' => $result['done']]; } /** @return array{items_removed: bool, items_retained: bool, messages: list, done: bool} */ public function erase(string $email, int $page = 1): array { $user = get_user_by('email', $email); if (!$user) { return ['items_removed' => false, 'items_retained' => false, 'messages' => [], 'done' => true]; } $result = $this->service()->erase(get_current_blog_id(), (int) $user->ID, $page); if ($result['done']) { // The per-user session-creation throttle counter (no content, but keyed to the user). delete_transient('aiwend_rl_' . substr(hash_hmac('sha256', 'u' . (int) $user->ID, wp_salt('nonce')), 0, 32)); $this->plugin->audit->record(get_current_blog_id(), 'system', 'privacy.erase', 'user:erased', 'ok', null, (string) $result['removed']); } return [ 'items_removed' => $result['removed'] > 0, 'items_retained' => $result['retained'] > 0, 'messages' => array_map([$this, 'message'], $result['messages']), 'done' => $result['done'], ]; } /** @param array{code:string, count:int} $m */ public function message(array $m): string { return match ($m['code']) { 'knowledge_retained' => sprintf( /* translators: %d: number of knowledge sources */ _n( '%d knowledge source uploaded by this user was kept as site content; the link to the user was removed. Delete it under Aiwend > Knowledge if it contains personal data.', '%d knowledge sources uploaded by this user were kept as site content; the link to the user was removed. Delete them under Aiwend > Knowledge if they contain personal data.', $m['count'], 'aiwend' ), $m['count'] ), 'local_only' => __('Aiwend data was deleted from this site\'s database only. If an external AI connection was used, contact the site owner about provider-held records and backups. Local deletion does not delete external records.', 'aiwend'), 'usage_retained' => __('Usage accounting records (anonymous request IDs and token counts) are kept for spending control; they contain no message content.', 'aiwend'), default => '', }; } public function policyText(): void { if (!function_exists('wp_add_privacy_policy_content')) { return; } wp_add_privacy_policy_content('Aiwend', wp_kses_post( '

' . __('When you use the AI assistant on this site, your messages and the assistant\'s answers are stored on this site so the conversation can continue. Conversations are deleted automatically after 30 days by default. A cookie (aiwend_chat_*) keeps your conversation private to your browser.', 'aiwend') . '

' . '

' . __('In simulated demo mode no external AI service is contacted. If the site owner assigns an external AI connection, instructions, conversation messages and selected source excerpts are sent to that provider. Review this site\'s provider and privacy disclosures before using live chat. Local erasure does not delete provider-held records or backups.', 'aiwend') . '

' . '

' . __('To limit abuse, Aiwend counts how many conversations an anonymous visitor starts, and how many messages are sent, in a short period. It does this with a keyed hash of the visitor\'s IP address (not the address itself) kept in a temporary WordPress setting for about 10 minutes (about one minute for the message count) and then discarded; behind a reverse proxy the address is the proxy\'s. Logged-in visitors are counted by their account number.', 'aiwend') . '

' . '

' . __('Site administrators who are allowed to read conversations can review stored questions and answers to provide support and improve the assistant\'s information. The conversations screen also shows counts and recurring unanswered questions; these are calculated from the stored conversations when the screen is opened, are not stored separately, and disappear when the conversation is deleted.', 'aiwend') . '

' . '

' . __('Logged-in users can ask the site owner to export or erase their assistant conversations using the standard WordPress privacy tools. Visitors without an account can delete their own conversation from the chat window.', 'aiwend') . '

' )); } private function service(): PrivacyService { return new PrivacyService($this->plugin->db, $this->plugin->sessions); } }