Pilih preferensi cookie Anda

Kami menggunakan cookie penting serta alat serupa yang diperlukan untuk menyediakan situs dan layanan. Kami menggunakan cookie performa untuk mengumpulkan statistik anonim sehingga kami dapat memahami cara pelanggan menggunakan situs dan melakukan perbaikan. Cookie penting tidak dapat dinonaktifkan, tetapi Anda dapat mengklik “Kustom” atau “Tolak” untuk menolak cookie performa.

Jika Anda setuju, AWS dan pihak ketiga yang disetujui juga akan menggunakan cookie untuk menyediakan fitur situs yang berguna, mengingat preferensi Anda, dan menampilkan konten yang relevan, termasuk iklan yang relevan. Untuk menerima atau menolak semua cookie yang tidak penting, klik “Terima” atau “Tolak”. Untuk membuat pilihan yang lebih detail, klik “Kustomisasi”.

SDK Pesan Klien Obrolan IVS: Tutorial React Native Bagian 1: Ruang Obrolan

Mode fokus
SDK Pesan Klien Obrolan IVS: Tutorial React Native Bagian 1: Ruang Obrolan - Amazon IVS

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

Ini adalah bagian pertama dari tutorial dua bagian. Anda akan mempelajari hal-hal penting dalam bekerja dengan Amazon IVS Chat Client Messaging JavaScript SDK dengan membangun aplikasi yang berfungsi penuh menggunakan React Native. Kami menyebut aplikasi itu Chatterbox.

Audiens yang dimaksudkan adalah para developer berpengalaman yang baru mengenal SDK Perpesanan Obrolan Amazon IVS. Anda harus merasa nyaman dengan TypeScript atau bahasa JavaScript pemrograman dan perpustakaan React Native.

Untuk singkatnya, kita akan merujuk ke Amazon IVS Chat Client Messaging JavaScript SDK sebagai Chat JS SDK.

Catatan: Dalam beberapa kasus, contoh kode untuk JavaScript dan TypeScript identik, sehingga digabungkan.

Bagian pertama dari tutorial ini dipecah menjadi beberapa bagian:

Prasyarat

Menyiapkan Server Autentikasi/Otorisasi Lokal

Aplikasi backend Anda bertanggung jawab untuk membuat ruang obrolan dan menghasilkan token obrolan yang diperlukan untuk SDK JS Obrolan guna mengautentikasi dan mengotorisasi klien Anda untuk ruang obrolan Anda. Anda harus menggunakan backend milik sendiri karena Anda tidak dapat menyimpan kunci AWS dengan aman di aplikasi seluler; penyerang canggih dapat mengekstraknya dan mendapatkan akses ke akun AWS Anda.

Lihat Buat Token Obrolan di Memulai Obrolan Amazon IVS. Seperti yang ditunjukkan pada diagram alur di sana, aplikasi sisi server Anda bertanggung jawab untuk membuat token obrolan. Hal ini berarti aplikasi Anda harus menyediakan caranya sendiri untuk menghasilkan token obrolan dengan memintanya dari aplikasi sisi server Anda.

Di bagian ini, Anda akan mempelajari dasar-dasar membuat penyedia token di backend Anda. Kami memanfaatkan kerangka kerja ekspres untuk membuat server lokal langsung yang mengelola pembuatan token obrolan menggunakan lingkungan AWS lokal Anda.

Buat proyek npm kosong menggunakan NPM. Buat direktori untuk menyimpan aplikasi Anda, dan jadikan sebagai direktori kerja Anda:

$ mkdir backend & cd backend

Gunakan npm init untuk membuat file package.json pada aplikasi Anda:

$ npm init

Perintah ini meminta beberapa hal pada Anda, termasuk nama dan versi aplikasi Anda. Saat ini, cukup tekan KEMBALI untuk menerima default sebagian besar perintah tersebut, dengan pengecualian berikut:

entry point: (index.js)

Tekan KEMBALI untuk menerima nama file default yang disarankan index.js atau masukkan nama apa pun yang Anda inginkan untuk file utama.

Sekarang instal dependensi yang diperlukan:

$ npm install express aws-sdk cors dotenv

aws-sdk memerlukan variabel lingkungan konfigurasi, yang memuat secara otomatis dari file bernama .env yang terletak di direktori root. Untuk mengonfigurasikannya, buat file baru bernama .env dan isi informasi konfigurasi yang hilang:

# .env # The region to send service requests to. AWS_REGION=us-west-2 # Access keys use an access key ID and secret access key # that you use to sign programmatic requests to AWS. # AWS access key ID. AWS_ACCESS_KEY_ID=... # AWS secret access key. AWS_SECRET_ACCESS_KEY=...

Sekarang kita buat file titik masuk di direktori root dengan nama yang Anda masukkan di atas dalam perintah npm init. Dalam hal ini, kami menggunakan index.js, dan mengimpor semua paket yang diperlukan:

// index.js import express from 'express'; import AWS from 'aws-sdk'; import 'dotenv/config'; import cors from 'cors';

Sekarang, buat instans baru express:

const app = express(); const port = 3000; app.use(express.json()); app.use(cors({ origin: ['http://127.0.0.1:5173'] }));

Setelah itu, Anda dapat membuat metode POST titik akhir pertama Anda untuk penyedia token. Ambil parameter yang diperlukan dari isi permintaan (roomId, userId, capabilities, dan sessionDurationInMinutes):

app.post('/create_chat_token', (req, res) => { const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {}; });

Tambahkan validasi bidang yang wajib diisi:

app.post('/create_chat_token', (req, res) => { const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {}; if (!roomIdentifier || !userId) { res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' }); return; } });

Setelah menyiapkan metode POST, kita mengintegrasikan createChatToken dengan aws-sdk untuk fungsionalitas inti autentikasi/otorisasi:

app.post('/create_chat_token', (req, res) => { const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {}; if (!roomIdentifier || !userId || !capabilities) { res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' }); return; } ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => { if (error) { console.log(error); res.status(500).send(error.code); } else if (data.token) { const { token, sessionExpirationTime, tokenExpirationTime } = data; console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`); res.json({ token, sessionExpirationTime, tokenExpirationTime }); } }); });

Di akhir file, tambahkan pendengar port untuk aplikasi express Anda:

app.listen(port, () => { console.log(`Backend listening on port ${port}`); });

Sekarang Anda dapat menjalankan server dengan perintah berikut dari root proyek:

$ node index.js

Tip: Server ini menerima permintaan URL di https://localhost:3000.

Buat Proyek Chatterbox

Pertama, Anda membuat proyek React Native yang disebut chatterbox. Jalankan perintah ini:

npx create-expo-app

Atau buat proyek expo dengan TypeScript template.

npx create-expo-app -t expo-template-blank-typescript

Anda dapat mengintegrasikan SDK JS Perpesanan Klien Obrolan melalui Manajer Paket Simpul atau Manajer Paket Yarn:

  • Npm: npm install amazon-ivs-chat-messaging

  • Yarn: yarn add amazon-ivs-chat-messaging

Menghubungkan ke Ruang Obrolan

Di sini Anda membuat ChatRoom dan menghubungkannya menggunakan metode asinkron. Kelas ChatRoom mengelola koneksi pengguna Anda ke SDK JS Obrolan. Agar berhasil terhubung ke ruang obrolan, Anda harus menyediakan instans ChatToken dalam aplikasi React Anda.

Arahkan ke file App yang dibuat dalam proyek chatterbox default dan hapus semua yang dikembalikan oleh komponen fungsional. Tidak memerlukan kode yang telah diisi sebelumnya. Saat ini, App kami cukup kosong.

TypeScript/JavaScript:

// App.tsx / App.jsx import * as React from 'react'; import { Text } from 'react-native'; export default function App() { return <Text>Hello!</Text>; }

Buat instans ChatRoom baru dan teruskan ke status menggunakan hook useState. Langkah ini mengharuskan penyediaan regionOrUrl (wilayah AWS tempat ruang obrolan Anda di-host) dan tokenProvider (digunakan untuk alur autentikasi/otorisasi backend yang dibuat pada langkah berikutnya).

Penting: Anda harus menggunakan wilayah AWS yang sama dengan wilayah tempat Anda membuat ruang di Memulai Obrolan Amazon IVS. API adalah layanan regional AWS. Untuk daftar wilayah yang didukung dan titik akhir layanan HTTPS Obrolan Amazon IVS, lihat halaman wilayah Obrolan Amazon IVS.

TypeScript/JavaScript:

// App.jsx / App.tsx import React, { useState } from 'react'; import { Text } from 'react-native'; import { ChatRoom } from 'amazon-ivs-chat-messaging'; export default function App() { const [room] = useState(() => new ChatRoom({ regionOrUrl: process.env.REGION, tokenProvider: () => {}, }), ); return <Text>Hello!</Text>; }

Membangun Penyedia Token

Sebagai langkah berikutnya, kita perlu membangun fungsi tokenProvider tanpa parameter yang diperlukan oleh konstruktor ChatRoom. Pertama, kita akan membuat fungsi fetchChatToken yang akan membuat permintaan POST ke aplikasi backend yang Anda siapkan di Menyiapkan Server Autentikasi/Otorisasi Lokal. Token obrolan berisi informasi yang diperlukan agar SDK berhasil membuat koneksi ruang obrolan. API Obrolan menggunakan token ini sebagai cara aman untuk memvalidasi identitas pengguna, kemampuan dalam ruang obrolan, dan durasi sesi.

Di navigator Proyek, buat JavaScript file TypeScript/baru bernamafetchChatToken. Bangun permintaan pengambilan ke aplikasi backend dan kembalikan objek ChatToken dari respons. Tambahkan properti isi permintaan yang diperlukan untuk membuat token obrolan. Gunakan aturan yang ditentukan untuk Amazon Resource Names (ARNs). Properti ini didokumentasikan dalam CreateChatTokenoperasi.

Catatan: URL yang Anda gunakan di sini adalah URL yang sama dengan yang dibuat oleh server lokal Anda saat menjalankan aplikasi backend.

TypeScript
// fetchChatToken.ts import { ChatToken } from 'amazon-ivs-chat-messaging'; type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE'; export async function fetchChatToken( userId: string, capabilities: UserCapability[] = [], attributes?: Record<string, string>, sessionDurationInMinutes?: number, ): Promise<ChatToken> { const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ userId, roomIdentifier: process.env.ROOM_ID, capabilities, sessionDurationInMinutes, attributes }), }); const token = await response.json(); return { ...token, sessionExpirationTime: new Date(token.sessionExpirationTime), tokenExpirationTime: new Date(token.tokenExpirationTime), }; }
JavaScript
// fetchChatToken.js export async function fetchChatToken( userId, capabilities = [], attributes, sessionDurationInMinutes) { const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ userId, roomIdentifier: process.env.ROOM_ID, capabilities, sessionDurationInMinutes, attributes }), }); const token = await response.json(); return { ...token, sessionExpirationTime: new Date(token.sessionExpirationTime), tokenExpirationTime: new Date(token.tokenExpirationTime), }; }
// fetchChatToken.ts import { ChatToken } from 'amazon-ivs-chat-messaging'; type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE'; export async function fetchChatToken( userId: string, capabilities: UserCapability[] = [], attributes?: Record<string, string>, sessionDurationInMinutes?: number, ): Promise<ChatToken> { const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ userId, roomIdentifier: process.env.ROOM_ID, capabilities, sessionDurationInMinutes, attributes }), }); const token = await response.json(); return { ...token, sessionExpirationTime: new Date(token.sessionExpirationTime), tokenExpirationTime: new Date(token.tokenExpirationTime), }; }

Mengamati Pembaruan Koneksi

Bereaksi terhadap perubahan status koneksi ruang obrolan adalah bagian penting dalam membuat aplikasi obrolan. Mari kita mulai dengan berlangganan peristiwa yang relevan:

TypeScript/JavaScript:

// App.tsx / App.jsx import React, { useState, useEffect } from 'react'; import { Text } from 'react-native'; import { ChatRoom } from 'amazon-ivs-chat-messaging'; import { fetchChatToken } from './fetchChatToken'; export default function App() { const [room] = useState( () => new ChatRoom({ regionOrUrl: process.env.REGION, tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']), }), ); useEffect(() => { const unsubscribeOnConnecting = room.addListener('connecting', () => {}); const unsubscribeOnConnected = room.addListener('connect', () => {}); const unsubscribeOnDisconnected = room.addListener('disconnect', () => {}); return () => { // Clean up subscriptions. unsubscribeOnConnecting(); unsubscribeOnConnected(); unsubscribeOnDisconnected(); }; }, [room]); return <Text>Hello!</Text>; }

Selanjutnya, kita perlu menyediakan kemampuan untuk membaca status koneksi. Kita menggunakan hook useState untuk membuat beberapa status lokal di App dan mengatur status koneksi di dalam setiap pendengar.

TypeScript/JavaScript:

// App.tsx / App.jsx import React, { useState, useEffect } from 'react'; import { Text } from 'react-native'; import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging'; import { fetchChatToken } from './fetchChatToken'; export default function App() { const [room] = useState( () => new ChatRoom({ regionOrUrl: process.env.REGION, tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']), }), ); const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected'); useEffect(() => { const unsubscribeOnConnecting = room.addListener('connecting', () => { setConnectionState('connecting'); }); const unsubscribeOnConnected = room.addListener('connect', () => { setConnectionState('connected'); }); const unsubscribeOnDisconnected = room.addListener('disconnect', () => { setConnectionState('disconnected'); }); return () => { unsubscribeOnConnecting(); unsubscribeOnConnected(); unsubscribeOnDisconnected(); }; }, [room]); return <Text>Hello!</Text>; }

Setelah berlangganan status koneksi, tampilkan status koneksi dan hubungkan ke ruang obrolan dengan menggunakan metode room.connect di dalam hook useEffect:

TypeScript/JavaScript:

// App.tsx / App.jsx // ... useEffect(() => { const unsubscribeOnConnecting = room.addListener('connecting', () => { setConnectionState('connecting'); }); const unsubscribeOnConnected = room.addListener('connect', () => { setConnectionState('connected'); }); const unsubscribeOnDisconnected = room.addListener('disconnect', () => { setConnectionState('disconnected'); }); room.connect(); return () => { unsubscribeOnConnecting(); unsubscribeOnConnected(); unsubscribeOnDisconnected(); }; }, [room]); // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> </SafeAreaView> ); const styles = StyleSheet.create({ root: { flex: 1, } }); // ...

Anda telah berhasil mengimplementasikan koneksi ruang obrolan.

Membuat Komponen Tombol Kirim

Di bagian ini, Anda membuat tombol kirim yang memiliki desain berbeda untuk setiap status koneksi. Tombol kirim memfasilitasi pengiriman pesan di ruang obrolan. Tombol ini juga berfungsi sebagai indikator visual mengenai apakah/kapan pesan dapat dikirim; misalnya, saat koneksi terputus atau sesi obrolan yang kedaluwarsa.

Pertama, buat file baru di direktori src proyek Chatterbox Anda dan beri nama SendButton. Kemudian, buat komponen yang akan menampilkan tombol untuk aplikasi obrolan Anda. Ekspor SendButton Anda dan impor ke App. Di <View></View> yang kosong, tambahkan <SendButton />.

TypeScript
// SendButton.tsx import React from 'react'; import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native'; interface Props { onPress?: () => void; disabled: boolean; loading: boolean; } export const SendButton = ({ onPress, disabled, loading }: Props) => { return ( <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}> {loading ? <Text>Send</Text> : <ActivityIndicator />} </TouchableOpacity> ); }; const styles = StyleSheet.create({ root: { width: 50, height: 50, borderRadius: 30, marginLeft: 10, justifyContent: 'center', alignContent: 'center', } }); // App.tsx import { SendButton } from './SendButton'; // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <SendButton /> </SafeAreaView> );
JavaScript
// SendButton.jsx import React from 'react'; import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native'; export const SendButton = ({ onPress, disabled, loading }) => { return ( <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}> {loading ? <Text>Send</Text> : <ActivityIndicator />} </TouchableOpacity> ); }; const styles = StyleSheet.create({ root: { width: 50, height: 50, borderRadius: 30, marginLeft: 10, justifyContent: 'center', alignContent: 'center', } }); // App.jsx import { SendButton } from './SendButton'; // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <SendButton /> </SafeAreaView> );
// SendButton.tsx import React from 'react'; import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native'; interface Props { onPress?: () => void; disabled: boolean; loading: boolean; } export const SendButton = ({ onPress, disabled, loading }: Props) => { return ( <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}> {loading ? <Text>Send</Text> : <ActivityIndicator />} </TouchableOpacity> ); }; const styles = StyleSheet.create({ root: { width: 50, height: 50, borderRadius: 30, marginLeft: 10, justifyContent: 'center', alignContent: 'center', } }); // App.tsx import { SendButton } from './SendButton'; // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <SendButton /> </SafeAreaView> );

Selanjutnya di App, tentukan fungsi bernama onMessageSend dan berikan fungsi tersebut ke properti SendButton onPress. Tentukan variabel lain bernama isSendDisabled (yang mencegah pengiriman pesan ketika ruang tidak terhubung) dan berikan variabel tersebut ke properti SendButton disabled.

TypeScript/JavaScript:

// App.jsx / App.tsx // ... const onMessageSend = () => {}; const isSendDisabled = connectionState !== 'connected'; return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <SendButton disabled={isSendDisabled} onPress={onMessageSend} /> </SafeAreaView> ); // ...

Buat Input Pesan

Bilah pesan Chatterbox adalah komponen yang akan berinteraksi dengan Anda untuk mengirim pesan ke ruang obrolan. Biasanya, bilah ini berisi input teks untuk menulis pesan dan tombol untuk mengirim pesan Anda.

Untuk membuat komponen MessageInput, pertama buat file baru di direktori src dan beri nama MessageInput. Kemudian, buat komponen yang akan menampilkan input untuk aplikasi obrolan Anda. Ekspor MessageInput Anda dan impor ke App (di atas <SendButton />).

Buat status baru bernama messageToSend menggunakan hook useState, dengan string kosong sebagai nilai default-nya. Di bagian utama aplikasi Anda, berikan messageToSend ke value dari MessageInput dan berikan setMessageToSend ke properti onMessageChange:

TypeScript
// MessageInput.tsx import * as React from 'react'; interface Props { value?: string; onValueChange?: (value: string) => void; } export const MessageInput = ({ value, onValueChange }: Props) => { return ( <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" /> ); }; const styles = StyleSheet.create({ input: { fontSize: 20, backgroundColor: 'rgb(239,239,240)', paddingHorizontal: 18, paddingVertical: 15, borderRadius: 50, flex: 1, } }) // App.tsx // ... import { MessageInput } from './MessageInput'; // ... export default function App() { const [messageToSend, setMessageToSend] = useState(''); // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <View style={styles.messageBar}> <MessageInput value={messageToSend} onMessageChange={setMessageToSend} /> <SendButton disabled={isSendDisabled} onPress={onMessageSend} /> </View> </SafeAreaView> ); const styles = StyleSheet.create({ root: { flex: 1, }, messageBar: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgb(160,160,160)', flexDirection: 'row', padding: 16, alignItems: 'center', backgroundColor: 'white', } });
JavaScript
// MessageInput.jsx import * as React from 'react'; export const MessageInput = ({ value, onValueChange }) => { return ( <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" /> ); }; const styles = StyleSheet.create({ input: { fontSize: 20, backgroundColor: 'rgb(239,239,240)', paddingHorizontal: 18, paddingVertical: 15, borderRadius: 50, flex: 1, } }) // App.jsx // ... import { MessageInput } from './MessageInput'; // ... export default function App() { const [messageToSend, setMessageToSend] = useState(''); // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <View style={styles.messageBar}> <MessageInput value={messageToSend} onMessageChange={setMessageToSend} /> <SendButton disabled={isSendDisabled} onPress={onMessageSend} /> </View> </SafeAreaView> ); const styles = StyleSheet.create({ root: { flex: 1, }, messageBar: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgb(160,160,160)', flexDirection: 'row', padding: 16, alignItems: 'center', backgroundColor: 'white', } });
// MessageInput.tsx import * as React from 'react'; interface Props { value?: string; onValueChange?: (value: string) => void; } export const MessageInput = ({ value, onValueChange }: Props) => { return ( <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" /> ); }; const styles = StyleSheet.create({ input: { fontSize: 20, backgroundColor: 'rgb(239,239,240)', paddingHorizontal: 18, paddingVertical: 15, borderRadius: 50, flex: 1, } }) // App.tsx // ... import { MessageInput } from './MessageInput'; // ... export default function App() { const [messageToSend, setMessageToSend] = useState(''); // ... return ( <SafeAreaView style={styles.root}> <Text>Connection State: {connectionState}</Text> <View style={styles.messageBar}> <MessageInput value={messageToSend} onMessageChange={setMessageToSend} /> <SendButton disabled={isSendDisabled} onPress={onMessageSend} /> </View> </SafeAreaView> ); const styles = StyleSheet.create({ root: { flex: 1, }, messageBar: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgb(160,160,160)', flexDirection: 'row', padding: 16, alignItems: 'center', backgroundColor: 'white', } });

Langkah Berikutnya

Karena Anda telah selesai membuat bilah pesan untuk Chatterbox, lanjutkan ke Bagian 2 tutorial React Native, Pesan dan Peristiwa.

PrivasiSyarat situsPreferensi cookie
© 2025, Amazon Web Services, Inc. atau afiliasinya. Semua hak dilindungi undang-undang.