
搜索引擎Typesense的使用
注: 使用语言 python一:Typesense介绍Typesense将数据保存在磁盘当中,建立的索引保存内存中 Typesense是一个开源的、有容错能力的搜索引擎,针对实时(通常低于 50 毫秒)搜索即键入体验和开发人员生产力进行了优化。 Typesense做了一个对于其他搜索引擎的对比。(文档版,表格版) 索引数据速度以及资源占用: 对于220万份食谱(一份食谱相当于下文中提到的一个document)在 Typesense 中进行索引时占用了大约 900MB 的 RAM(内存)花了 3.6 分钟索引所有 220 万条记录在具有 4 个 vCPU 的服务器上,Typesense 每秒能够处理104 个并发搜索查询,平均搜索处理时间为11毫秒。RAM(内存)方面:如果数据量为 X MB大小,则需要占用2X-3XRAM(2-3倍数据量大小的占用) 如需深入了解可以查阅官方文档二:Typesense的用法1:使用typesense有两种方法使用自带的云服务,配置运行简单(收费)在本地安装typesense,自己维护配置(本文使用这种方法)2:安装启动typesense(1):下载...

Mastodon 和 Nostr:两种不同的社交产品,一样的去中心化愿景
概述:本文将分析和讲解Mastodon和Nostr这两个社交媒体平台,重点关注它们的产品应用和技术层面,以了解它们如何实现去中心化社交,并探讨它们在这方面的优势。我们将深入了解它们的架构设计和实现思路,并比较它们在用户体验、隐私保护、安全性等方面的差异。通过本文的分析和总结,读者将更好地了解这两个平台,以及它们在去中心化社交方面的贡献和发展。MastodonMastodon(长毛象)成立于2016年,是由Eugen Rochko创建的一个开源的微博客(microblog)平台,旨在为用户提供去中心化、隐私保护的社交体验。首先对涉及到的一些名词进行简单解释:联邦(federation):联邦是去中心化的一种形式。在联邦中,不是所有人共同使用一个中心服务,而是使用多个不限人数的服务器。ActivityPub:Mastodon使用一种标准化的、开放的协议来实现站点之间的互动,这种协议叫做ActivityPub。任何通过ActivityPub实现互联的软件都可以与Mastodon无缝通信,就像Mastodon站点之间的通信一样。实例(instance):每个人都可以在自己服务器上配置运行...

使用 The Graph 获取各大元宇宙项目交易信息
The graph 工作原理 Graph 根据subgraph描述(称为subgraph.graphq)学习什么以及如何索引以太坊数据。子图描述定义了subgraph感兴趣的智能合约,这些合约中要关注的事件,以及如何将事件数据映射到 The Graph 将存储在其数据库中的数据。该流程遵循以下步骤:去中心化应用程序通过智能合约上的交易将数据添加到以太坊。智能合约在处理交易时发出一个或多个事件。Graph Node 不断地扫描以太坊以寻找新的块以及它们可能包含的子图的数据。Graph Node 在这些块中为您的子图查找 Ethereum 事件并运行您提供的映射处理程序。映射是一个 WASM 模块,它创建或更新 Graph Node 存储的数据实体以响应以太坊事件。去中心化应用程序使用节点的GraphQL 端点查询 Graph 节点以获取从区块链索引的数据。Graph 节点反过来将 GraphQL 查询转换为对其底层数据存储的查询,以便利用存储的索引功能获取此数据。去中心化应用程序在丰富的 UI 中为最终用户显示这些数据,他们使用这些数据在以太坊上发布新交易。循环重复 (来自The ...
操作系统: macos 13.0.1
python版本:3.9.7
eth-brownie版本:1.19.2
本文使用开发语言python,用基于python的开发和测试框架Brownie来开发和部署合约,该空投合约可以对Decentranland Wearables进行批量空投。
1: 在Infura中创建一个Polygon的project(Decentranland Wearables在polygon链上)
2:安装eth-brownie
pip install eth-brownie

3: 初始化项目
创建个空文件夹,执行初始化命令,之后会生成一些文件夹
mkdir air_drop
cd air_drop
brownie init

4: 在contracts 文件夹中来创建.sol文件来写入合约代码
cd contracts/
touch air_drop_batch.sol
首先是两个interface基于ERC165和ERC721,这里是固定的直接写入即可,也可以把这两个interface放入文件夹interfaces/中,然后在contracts/air_drop_batch.sol调用,这里我们直接放到一个文件中。
为什么是ERC721?
来随便找个decentranland wearable来看下


接下来就是要写对于空投逻辑处理的合约,接上面代码
contract AirdropBatchNFT{
event Airdrop721(
address from,
address batchContractAddress
);
function ERC721AirdropBatch(
address from, // 发送wearable用户的地址
address batchContractAddress, // 要发送的wearable的合约地址
uint256[] memory tokenIdSet, // 发送wearable的tokenid 列表
address[] memory receivers, // 接收空投wearable的用户钱包地址列表
) public {
// 判断是否是发送者自己操作
require(msg.sender == from, "danger sender");
// 判断发送者是否对本空投合约授权,只有授权空投合约才可以对发送者的wearable进行空投 require(IERC721(batchContractAddress).isApprovedForAll(msg.sender, address(this)), "not approve");
// 接收者列表的长度要与空投的tokenid列表长度一样
require(tokenIdSet.length == receivers.length, "The length of tokenIdSet is different from the length of receivers");
// 判断要空投的wearable是否都属于空投者
for(uint256 j = 0; j < tokenIdSet.length; ++j) { require(IERC721(batchContractAddress).ownerOf(tokenIdSet[j]) == from, "tokens not enough");
}
// 把两个列表中第n个token空投给弟n个接受者
for(uint256 i = 0; i < receivers.length; ++i) {
IERC721(batchContractAddress).safeTransferFrom(from, receivers[i], tokenIdSet[i]);
}
emit Airdrop721(
from,
batchContractAddress
);
}
项目根目录创建配置文件
touch brownie-config.yaml
dependencies:
- OpenZeppelin/openzeppelin-contracts@4.6.0 # 在上文中提到的IERC721&IERC165 可以从OpenZeppelin中导入,这里也指定下版本,只不过上文中是直接填的源码,并不是导入
# 如果使用 openzeppelin 需要下载
# brownie pm install OpenZeppelin/openzeppelin-contracts@4.6.0
compiler:
solc:
version: null
optimizer:
enabled: true
runs: 200
remappings:
- '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0'
# .env 由于WEB3_INFURA_PROJECT_ID和PRIVATE_KEY 存放在文件中
dotenv: .env
networks:
default: polygon-main
polygon-main:
host: https://polygon-mainnet.infura.io/v3/${WEB3_INFURA_PROJECT_ID} # 上面创建的infura项目的连接
wallets:
PRIVATE_KEY: ${PRIVATE_KEY} # 钱包地址的私钥
项目根创建 .env 存放私钥等信息
touch .env
PRIVATE_KEY=xxxxxxxxx
WEB3_INFURA_PROJECT_ID=xxxxxxxxxx
终端执行 brownie compile编译代码,生成abi文件 接下来是部署,如果部署失败原因指定WEB3_INFURA_PROJECT_ID 或是PRIVATE_KEY
source .env
检查两个值是否正确
在scripts/中创建depoly.py文件
cd scripts/
touch deploy.py
from brownie import AirdropBatchNFT, config, accounts # AirdropBatchNFT 是合约中的构造函数,项目要以brownie来启动
def get_account():
return accounts.add(config['wallets']['PRIVATE_KEY'])
def test(account):
air_drop = AirdropBatchNFT.deploy({'from': account})
def main():
account = get_account()
test(account)
print('account=', account)
接下来执行部署命令
brownie run deploy.py
之后会看到部署的合约地址以及在 build/deployments/map.json中也可以看到部署的纪录





bytecode 可以不填
optimization 以及下方settings中的内容配置可以在 build/depolyments/137/合约地址.json中找到 搜索compiler


这样,Decentranland 空投wearable的合约就完成了!
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
操作系统: macos 13.0.1
python版本:3.9.7
eth-brownie版本:1.19.2
本文使用开发语言python,用基于python的开发和测试框架Brownie来开发和部署合约,该空投合约可以对Decentranland Wearables进行批量空投。
1: 在Infura中创建一个Polygon的project(Decentranland Wearables在polygon链上)
2:安装eth-brownie
pip install eth-brownie

3: 初始化项目
创建个空文件夹,执行初始化命令,之后会生成一些文件夹
mkdir air_drop
cd air_drop
brownie init

4: 在contracts 文件夹中来创建.sol文件来写入合约代码
cd contracts/
touch air_drop_batch.sol
首先是两个interface基于ERC165和ERC721,这里是固定的直接写入即可,也可以把这两个interface放入文件夹interfaces/中,然后在contracts/air_drop_batch.sol调用,这里我们直接放到一个文件中。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
为什么是ERC721?
来随便找个decentranland wearable来看下


接下来就是要写对于空投逻辑处理的合约,接上面代码
contract AirdropBatchNFT{
event Airdrop721(
address from,
address batchContractAddress
);
function ERC721AirdropBatch(
address from, // 发送wearable用户的地址
address batchContractAddress, // 要发送的wearable的合约地址
uint256[] memory tokenIdSet, // 发送wearable的tokenid 列表
address[] memory receivers, // 接收空投wearable的用户钱包地址列表
) public {
// 判断是否是发送者自己操作
require(msg.sender == from, "danger sender");
// 判断发送者是否对本空投合约授权,只有授权空投合约才可以对发送者的wearable进行空投 require(IERC721(batchContractAddress).isApprovedForAll(msg.sender, address(this)), "not approve");
// 接收者列表的长度要与空投的tokenid列表长度一样
require(tokenIdSet.length == receivers.length, "The length of tokenIdSet is different from the length of receivers");
// 判断要空投的wearable是否都属于空投者
for(uint256 j = 0; j < tokenIdSet.length; ++j) { require(IERC721(batchContractAddress).ownerOf(tokenIdSet[j]) == from, "tokens not enough");
}
// 把两个列表中第n个token空投给弟n个接受者
for(uint256 i = 0; i < receivers.length; ++i) {
IERC721(batchContractAddress).safeTransferFrom(from, receivers[i], tokenIdSet[i]);
}
emit Airdrop721(
from,
batchContractAddress
);
}
项目根目录创建配置文件
touch brownie-config.yaml
dependencies:
- OpenZeppelin/openzeppelin-contracts@4.6.0 # 在上文中提到的IERC721&IERC165 可以从OpenZeppelin中导入,这里也指定下版本,只不过上文中是直接填的源码,并不是导入
# 如果使用 openzeppelin 需要下载
# brownie pm install OpenZeppelin/openzeppelin-contracts@4.6.0
compiler:
solc:
version: null
optimizer:
enabled: true
runs: 200
remappings:
- '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0'
# .env 由于WEB3_INFURA_PROJECT_ID和PRIVATE_KEY 存放在文件中
dotenv: .env
networks:
default: polygon-main
polygon-main:
host: https://polygon-mainnet.infura.io/v3/${WEB3_INFURA_PROJECT_ID} # 上面创建的infura项目的连接
wallets:
PRIVATE_KEY: ${PRIVATE_KEY} # 钱包地址的私钥
项目根创建 .env 存放私钥等信息
touch .env
PRIVATE_KEY=xxxxxxxxx
WEB3_INFURA_PROJECT_ID=xxxxxxxxxx
终端执行 brownie compile编译代码,生成abi文件 接下来是部署,如果部署失败原因指定WEB3_INFURA_PROJECT_ID 或是PRIVATE_KEY
source .env
检查两个值是否正确
在scripts/中创建depoly.py文件
cd scripts/
touch deploy.py
from brownie import AirdropBatchNFT, config, accounts # AirdropBatchNFT 是合约中的构造函数,项目要以brownie来启动
def get_account():
return accounts.add(config['wallets']['PRIVATE_KEY'])
def test(account):
air_drop = AirdropBatchNFT.deploy({'from': account})
def main():
account = get_account()
test(account)
print('account=', account)
接下来执行部署命令
brownie run deploy.py
之后会看到部署的合约地址以及在 build/deployments/map.json中也可以看到部署的纪录





bytecode 可以不填
optimization 以及下方settings中的内容配置可以在 build/depolyments/137/合约地址.json中找到 搜索compiler


这样,Decentranland 空投wearable的合约就完成了!

搜索引擎Typesense的使用
注: 使用语言 python一:Typesense介绍Typesense将数据保存在磁盘当中,建立的索引保存内存中 Typesense是一个开源的、有容错能力的搜索引擎,针对实时(通常低于 50 毫秒)搜索即键入体验和开发人员生产力进行了优化。 Typesense做了一个对于其他搜索引擎的对比。(文档版,表格版) 索引数据速度以及资源占用: 对于220万份食谱(一份食谱相当于下文中提到的一个document)在 Typesense 中进行索引时占用了大约 900MB 的 RAM(内存)花了 3.6 分钟索引所有 220 万条记录在具有 4 个 vCPU 的服务器上,Typesense 每秒能够处理104 个并发搜索查询,平均搜索处理时间为11毫秒。RAM(内存)方面:如果数据量为 X MB大小,则需要占用2X-3XRAM(2-3倍数据量大小的占用) 如需深入了解可以查阅官方文档二:Typesense的用法1:使用typesense有两种方法使用自带的云服务,配置运行简单(收费)在本地安装typesense,自己维护配置(本文使用这种方法)2:安装启动typesense(1):下载...

Mastodon 和 Nostr:两种不同的社交产品,一样的去中心化愿景
概述:本文将分析和讲解Mastodon和Nostr这两个社交媒体平台,重点关注它们的产品应用和技术层面,以了解它们如何实现去中心化社交,并探讨它们在这方面的优势。我们将深入了解它们的架构设计和实现思路,并比较它们在用户体验、隐私保护、安全性等方面的差异。通过本文的分析和总结,读者将更好地了解这两个平台,以及它们在去中心化社交方面的贡献和发展。MastodonMastodon(长毛象)成立于2016年,是由Eugen Rochko创建的一个开源的微博客(microblog)平台,旨在为用户提供去中心化、隐私保护的社交体验。首先对涉及到的一些名词进行简单解释:联邦(federation):联邦是去中心化的一种形式。在联邦中,不是所有人共同使用一个中心服务,而是使用多个不限人数的服务器。ActivityPub:Mastodon使用一种标准化的、开放的协议来实现站点之间的互动,这种协议叫做ActivityPub。任何通过ActivityPub实现互联的软件都可以与Mastodon无缝通信,就像Mastodon站点之间的通信一样。实例(instance):每个人都可以在自己服务器上配置运行...

使用 The Graph 获取各大元宇宙项目交易信息
The graph 工作原理 Graph 根据subgraph描述(称为subgraph.graphq)学习什么以及如何索引以太坊数据。子图描述定义了subgraph感兴趣的智能合约,这些合约中要关注的事件,以及如何将事件数据映射到 The Graph 将存储在其数据库中的数据。该流程遵循以下步骤:去中心化应用程序通过智能合约上的交易将数据添加到以太坊。智能合约在处理交易时发出一个或多个事件。Graph Node 不断地扫描以太坊以寻找新的块以及它们可能包含的子图的数据。Graph Node 在这些块中为您的子图查找 Ethereum 事件并运行您提供的映射处理程序。映射是一个 WASM 模块,它创建或更新 Graph Node 存储的数据实体以响应以太坊事件。去中心化应用程序使用节点的GraphQL 端点查询 Graph 节点以获取从区块链索引的数据。Graph 节点反过来将 GraphQL 查询转换为对其底层数据存储的查询,以便利用存储的索引功能获取此数据。去中心化应用程序在丰富的 UI 中为最终用户显示这些数据,他们使用这些数据在以太坊上发布新交易。循环重复 (来自The ...

Subscribe to Yooma

Subscribe to Yooma
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
No activity yet