├── runtime.txt ├── Procfile ├── hgbr_final.pickle ├── .gitignore ├── helpers ├── sample_jsonURI.py ├── sample_metadata.py ├── predict_price.py ├── helpers.py ├── functions.py └── create_URI.py ├── README.md ├── requirements.txt └── app.py /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.1 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 app.py 2 | -------------------------------------------------------------------------------- /hgbr_final.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorykop/telegram-bot/HEAD/hgbr_final.pickle -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | helpers/__pycache__/ 3 | credentials.py 4 | helpers/models.py 5 | 6 | .csv 7 | .pickle 8 | vars.py 9 | req.txt 10 | reqs.txt 11 | *.csv 12 | *.env -------------------------------------------------------------------------------- /helpers/sample_jsonURI.py: -------------------------------------------------------------------------------- 1 | sample_URI = { 2 | "name" : "", 3 | "description": "Property NFT", 4 | "image" : "", 5 | "attributes": [ 6 | { 7 | "id": "", 8 | "country": "", 9 | "region": "", 10 | "city": "", 11 | "street": "", 12 | "number": "", 13 | "cap": "", 14 | "property_type": "", 15 | "floors": "", 16 | "size": "" 17 | } 18 | 19 | 20 | ] 21 | 22 | } 23 | -------------------------------------------------------------------------------- /helpers/sample_metadata.py: -------------------------------------------------------------------------------- 1 | sample_URI = { 2 | "name" : "", 3 | "description": "Property NFT", 4 | "image" : "", 5 | "attributes": [ 6 | { 7 | "id": "", 8 | "country": "", 9 | "region": "", 10 | "city": "", 11 | "street": "", 12 | "number": "", 13 | "cap": "", 14 | "property_type": "", 15 | "floors": "", 16 | "size": "" 17 | } 18 | 19 | 20 | ] 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## @LandReg_bot 2 | 3 | 4 | 5 | 6 | #### Telegram bot for blockchain-based land registry 7 | 8 | ##### The bot allows you to: 9 | * Register a property ownership on a blockchain 10 | * Apply for a NFT on a property 11 | * Get a price estimation of a property 12 | 13 | Try it out with our [Land registry bot](https://t.me/LandReg_bot) 14 | 15 | Stack: 16 | * [Telegram Bot](https://t.me/LandReg_bot) 17 | * [Pinata](https://app.pinata.cloud/) for storing NFT metadata 18 | -------------------------------------------------------------------------------- /helpers/predict_price.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import pandas as pd 3 | import numpy as np 4 | from sklearn.base import TransformerMixin 5 | 6 | def predict(surface, rooms, floor): 7 | 8 | filename = 'hgbr_final.pickle' 9 | hgbr = pickle.load(open(filename, 'rb')) 10 | 11 | test = pd.DataFrame(data={'Surface' : [int(surface)], 'Rooms' : str(rooms), 'Floor' : str(floor)}) 12 | 13 | return int(np.exp(hgbr.predict(test))) 14 | 15 | class DenseTransformer(TransformerMixin): 16 | 17 | def fit(self, X, y=None, **fit_params): 18 | return self 19 | 20 | def transform(self, X, y=None, **fit_params): 21 | return X.todense() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argcomplete==1.12.3 2 | click==8.0.3 3 | mongoengine==0.23.1 4 | packaging==21.3 5 | pi==0.1.2 6 | pipx==0.16.4 7 | pymongo==3.12.3 8 | pyparsing==3.0.6 9 | userpath==1.7.0 10 | aiohttp==3.8.1 11 | aiosignal==1.2.0 12 | anyio==3.4.0 13 | APScheduler==3.6.3 14 | async-timeout==4.0.1 15 | attrs==21.2.0 16 | base58==2.1.1 17 | bitarray==1.2.2 18 | brotlipy==0.7.0 19 | cachetools==4.2.2 20 | certifi==2021.10.8 21 | cffi==1.15.0 22 | chardet==3.0.4 23 | charset-normalizer==2.0.9 24 | cryptography==36.0.0 25 | cytoolz==0.11.2 26 | dnspython==2.2.0rc1 27 | eth-abi==2.1.1 28 | eth-account==0.5.6 29 | eth-hash==0.3.2 30 | eth-keyfile==0.5.1 31 | eth-keys==0.3.3 32 | eth-rlp==0.2.1 33 | eth-typing==2.2.2 34 | eth-utils==1.10.0 35 | Flask==1.1.2 36 | Flask-SQLAlchemy==2.5.1 37 | Flask-WTF==0.14.3 38 | frozenlist==1.2.0 39 | greenlet==1.1.2 40 | gunicorn==20.1.0 41 | h11==0.12.0 42 | h2==4.1.0 43 | hexbytes==0.2.2 44 | hpack==4.0.0 45 | httpcore==0.14.3 46 | httpx==0.21.1 47 | hyperframe==6.0.1 48 | idna==2.10 49 | importlib-metadata==4.8.2 50 | itsdangerous==1.1.0 51 | Jinja2==2.11.2 52 | jsonschema==3.2.0 53 | lru-dict==1.1.7 54 | MarkupSafe==2.0.1 55 | multiaddr==0.0.9 56 | multidict==5.2.0 57 | netaddr==0.7.19 58 | pandas==1.3.5 59 | parsimonious==0.8.1 60 | pip==21.3.1 61 | protobuf==3.19.1 62 | pycparser==2.21 63 | pycryptodome==3.12.0 64 | pyOpenSSL==19.1.0 65 | pyrsistent==0.18.0 66 | PySocks==1.7.1 67 | python-dotenv==0.19.2 68 | python-telegram-bot==13.9 69 | pytz==2021.3 70 | pytz-deprecation-shim==0.1.0.post0 71 | requests==2.24.0 72 | rfc3986==1.5.0 73 | rlp==1.2.0 74 | setuptools==59.4.0 75 | scikit-learn=1.0.1 76 | six==1.16.0 77 | sniffio==1.2.0 78 | SQLAlchemy==1.4.28 79 | toolz==0.11.2 80 | tornado==6.1 81 | typing_extensions==4.0.1 82 | tzdata==2021.5 83 | tzlocal==4.1 84 | ujson==4.2.0 85 | urllib3==1.25.11 86 | varint==1.0.2 87 | web3==5.25.0 88 | websockets==9.1 89 | Werkzeug==1.0.1 90 | wheel==0.37.0 91 | WTForms==2.3.3 92 | yarl==1.7.2 93 | zipp==3.6.0 94 | 95 | -------------------------------------------------------------------------------- /helpers/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Dict 3 | 4 | 5 | def facts_to_str(user_data: Dict[str, str]) -> str: 6 | """Helper function for formatting the gathered user info.""" 7 | 8 | user_facts = [f'{key}: {value}' for key, value in user_data.items() if key in \ 9 | ['First name', 'Last name', 'Doc type', 'Doc number', 'Fiscal code', 'wallet address']] 10 | property_facts = [f'{key}: {value}' for key, value in user_data.items() if key in \ 11 | ['Country', 'Region', 'City', 'Street', 'Buildnig number', 'Cap', 'Property type', 12 | 'Floors', 'Property size']] 13 | 14 | return "\n".join(user_facts + property_facts).join(['\n', '\n']) 15 | 16 | def user_info_dict(user_data: Dict[str, str]) -> str: 17 | user_facts = {key:value for key, value in user_data.items() if key in \ 18 | ['id', 'First name', 'Last name', 'Doc type', 'Doc number', 'Fiscal code']} 19 | 20 | return user_facts 21 | 22 | def property_info_dict(user_data: Dict[str, str]) -> str: 23 | property_facts = [{key: value for key, value in user_data.items() if key in \ 24 | ['id', 'Country', 'Region', 'City', 'Street', 'Buildnig number', 'Cap', 'Property type', 25 | 'Floors', 'Property size']}] 26 | 27 | return property_facts 28 | 29 | def handle_message(update, context): 30 | text = str(update.message.text).lower() 31 | return text 32 | 33 | def get_owner_data(context): 34 | 35 | firstName = context.user_data['First name'] 36 | lastName = context.user_data['Last name'] 37 | owner_address = context.user_data['wallet address'] 38 | codiceFiscale = context.user_data['Fiscal code'] 39 | docType = context.user_data['Doc type'] 40 | docNumber = context.user_data['Doc number'] 41 | 42 | return (firstName, lastName, owner_address, codiceFiscale, docType, docNumber) 43 | 44 | def get_property_data(context): 45 | Owner_address = context.user_data['wallet address'] 46 | areaSqm = context.user_data['Property size'] 47 | floor = context.user_data['Floors'] 48 | zipCode = context.user_data['Cap'] 49 | country = context.user_data['Country'] 50 | region = context.user_data['Region'] 51 | city = context.user_data['City'] 52 | street = context.user_data['Street'] 53 | streetNumber = context.user_data['Building number'] 54 | addressAdditional = 'No additional info' 55 | houseType = context.user_data['Property type'] 56 | 57 | return (Owner_address, int(areaSqm), int(floor), int(zipCode), country, region, city, street, streetNumber, addressAdditional, houseType) 58 | 59 | -------------------------------------------------------------------------------- /helpers/functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from web3 import Web3 3 | 4 | def RegisterOwner(URL, contract, abi,_firstName,_lastName,owner_address, _codiceFiscale, _docType, _docNumber): # all inputs are strings 5 | #connect to the contract 6 | web3 = Web3(Web3.HTTPProvider(URL)) 7 | web3.eth.defaultAccount = web3.toChecksumAddress('0x5face5582CaE06bE3E51B05DA2c3853D353A5CC5') 8 | # web3.eth.defaultAccount = web3.eth.accounts.privateKeyToAccount('0x2c092433de46556c8569194b39474cc058389a2245242591eb7640eb8034a4af') 9 | abi = json.loads(abi) 10 | address = web3.toChecksumAddress(contract) 11 | print("Starting registring...") 12 | contract = web3.eth.contract(address = address, abi = abi) # connect to the subscription/registry contract 13 | print("Connected to the contract...") 14 | tx = contract.functions.registerOwner(_firstName,_lastName,owner_address, _codiceFiscale, _docType, _docNumber).buildTransaction({'nonce': web3.eth.getTransactionCount(web3.eth.defaultAccount)}) 15 | signed_tx = web3.eth.account.signTransaction(tx, private_key='0x2c092433de46556c8569194b39474cc058389a2245242591eb7640eb8034a4af') 16 | web3.eth.sendRawTransaction(signed_tx.rawTransaction) 17 | 18 | 19 | def RegisterProperty(URL, contract, abi, _Owner_address, _areaSqm, _floor, _zipCode, _country, _region, _city, _street, _streetNumber, _addressAdditional, _houseType): 20 | web3 = Web3(Web3.HTTPProvider(URL)) 21 | web3.eth.defaultAccount = web3.toChecksumAddress('0x5face5582CaE06bE3E51B05DA2c3853D353A5CC5') 22 | abi = json.loads(abi) 23 | address = web3.toChecksumAddress(contract) 24 | print("Starting registring...") 25 | contract = web3.eth.contract(address = address, abi = abi) # connect to the subscription/registry contract 26 | print("Connected to the contract...") 27 | print(_Owner_address, _areaSqm, _floor, _zipCode, _country, _region, _city, _street, _streetNumber, _addressAdditional, _houseType) 28 | tx = contract.functions.registerProperty(_Owner_address, _areaSqm, _floor, _zipCode, _country, _region, _city, _street, _streetNumber, _addressAdditional, _houseType).buildTransaction({'nonce': web3.eth.getTransactionCount(web3.eth.defaultAccount)}) 29 | signed_tx = web3.eth.account.signTransaction(tx, private_key='0x2c092433de46556c8569194b39474cc058389a2245242591eb7640eb8034a4af') 30 | web3.eth.sendRawTransaction(signed_tx.rawTransaction) 31 | 32 | def MintNFT(URL,contract,abi, uri, owner_address): # all inputs are strings 33 | #connect to the contract 34 | web3 = Web3(Web3.HTTPProvider(URL)) 35 | web3.eth.defaultAccount = web3.toChecksumAddress('0x5face5582CaE06bE3E51B05DA2c3853D353A5CC5') 36 | abi = json.loads(abi) 37 | address = web3.toChecksumAddress(contract) 38 | contract = web3.eth.contract(address = address, abi = abi) # connect to the nft contract 39 | print("Connected to the contract...") 40 | tx = contract.functions.mintNFT(uri, owner_address).buildTransaction({'nonce': web3.eth.getTransactionCount(web3.eth.defaultAccount)}) 41 | signed_tx = web3.eth.account.signTransaction(tx, private_key='0x2c092433de46556c8569194b39474cc058389a2245242591eb7640eb8034a4af') 42 | web3.eth.sendRawTransaction(signed_tx.rawTransaction) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /helpers/create_URI.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | from pathlib import Path 4 | from dotenv import load_dotenv 5 | import json 6 | import csv 7 | import os 8 | import time 9 | import helpers.sample_jsonURI 10 | import numpy as np 11 | 12 | load_dotenv() 13 | 14 | PINATA_API_KEY = os.environ.get('PINATA_API_KEY') 15 | PINATA_API_SECRET = os.environ.get('PINATA_API_SECRET') 16 | #Pinata API 17 | # API_Key = 'ccbebfd0bead0305d402' 18 | # API_Secret = '0246b9b984a25470d524a988d70f276d621d49a9c58f299283bd8bf8e4563bf5' 19 | # JWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiI2YWYyOGIxMS03OGIyLTRkYjEtYTI1Yy1hNWJiYjQ0NmQ5NTIiLCJlbWFpbCI6ImJlYS5ndWlkb3R0aUBsaWJlcm8uaXQiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGluX3BvbGljeSI6eyJyZWdpb25zIjpbeyJpZCI6IkZSQTEiLCJkZXNpcmVkUmVwbGljYXRpb25Db3VudCI6MX1dLCJ2ZXJzaW9uIjoxfSwibWZhX2VuYWJsZWQiOmZhbHNlfSwiYXV0aGVudGljYXRpb25UeXBlIjoic2NvcGVkS2V5Iiwic2NvcGVkS2V5S2V5IjoiY2NiZWJmZDBiZWFkMDMwNWQ0MDIiLCJzY29wZWRLZXlTZWNyZXQiOiIwMjQ2YjliOTg0YTI1NDcwZDUyNGE5ODhkNzBmMjc2ZDYyMWQ0OWE5YzU4ZjI5OTI4M2JkOGJmOGU0NTYzYmY1IiwiaWF0IjoxNjM5OTMxMTAyfQ.WgnOW9731uvFsgR7kDxkJ_WtBLX5Sl5UDoE1JQxW6RE' 20 | 21 | PINATA_BASE_URL = 'https://api.pinata.cloud/' 22 | 23 | ENDPOINT = 'pinning/pinFileToIPFS' 24 | HEADERS = {'pinata_api_key': os.getenv('PINATA_API_KEY'), 25 | 'pinata_secret_api_key': os.getenv('PINATA_API_SECRET')} 26 | 27 | def write_metadata(property_id, property_size, floors, cap, property_address_country, property_address_region, 28 | property_address_city, property_address_street, property_address_streetnum, 29 | addressAdditional, property_type): 30 | 31 | jsonfile = helpers.sample_jsonURI.sample_URI 32 | 33 | metadata_file_name = ( 34 | "metadata_{}.json".format(str(property_id)) 35 | ) 36 | if Path(metadata_file_name).exists(): 37 | print( 38 | "{} already found, delete it to overwrite!".format( 39 | metadata_file_name) 40 | ) 41 | else: 42 | print("Creating Metadata file: " + metadata_file_name) 43 | jsonfile['attributes'][0]['id'] = property_id 44 | jsonfile['attributes'][0]['country'] = property_address_country 45 | jsonfile['attributes'][0]['region'] = property_address_region 46 | jsonfile['attributes'][0]['city'] = property_address_city 47 | jsonfile['attributes'][0]['street'] = property_address_street 48 | jsonfile['attributes'][0]['number'] = property_address_streetnum 49 | jsonfile['attributes'][0]['cap'] = cap 50 | jsonfile['attributes'][0]['property_type'] = property_type 51 | jsonfile['attributes'][0]['floors'] = floors 52 | jsonfile['attributes'][0]['property_size'] = property_size 53 | 54 | with open(metadata_file_name, "w") as file: 55 | json.dump(jsonfile, file) 56 | 57 | # Function that creates a json file with the metadata in the proper format 58 | def create_URI(property_size, floors, cap, property_address_country, property_address_region, 59 | property_address_city, property_address_street, property_address_streetnum, 60 | addressAdditional, property_type): 61 | 62 | property_id = np.random.randint(0,1000000) 63 | 64 | filename = "metadata_{}".format(property_id) 65 | item = {'filename': filename} 66 | 67 | # storing all uplaoded metadata URIs 68 | fileWriter = open('uploadedNFT.csv','a', encoding="utf-8") 69 | 70 | if not os.path.exists('uploadedNFT.csv'): 71 | fieldnames = ['filename','ipfsHash', 'URI'] 72 | dictWriter = csv.DictWriter(fileWriter, fieldnames) 73 | dictWriter.writeheader() 74 | else: 75 | fieldnames = ['filename','ipfsHash', 'URI'] 76 | dictWriter = csv.DictWriter(fileWriter, fieldnames) 77 | 78 | #load the sample json file 79 | write_metadata(property_id, property_size, floors, cap, property_address_country, property_address_region, \ 80 | property_address_city, property_address_street, property_address_streetnum, \ 81 | addressAdditional, property_type) 82 | 83 | #Create the URI 84 | resp = requests.post(PINATA_BASE_URL + ENDPOINT, 85 | files={"file": (filename, open(f'{filename}.json'))}, 86 | headers=HEADERS) 87 | retry=0 88 | while(resp.status_code != 200 and retry < 3): 89 | retry +=1 90 | print("attempt {}...".format(retry+1),end='',flush=True) 91 | time.sleep(15) 92 | resp = requests.post(PINATA_BASE_URL + ENDPOINT, 93 | files={"file": (filename, open(f'{filename}.json'))}, 94 | headers=HEADERS) 95 | 96 | if(resp.status_code == 200): 97 | 98 | print(f"{filename} upload successful") 99 | 100 | ipfs_hash = resp.json()['IpfsHash'] 101 | token_uri = "https://ipfs.io/ipfs/{}?filename={}".format(ipfs_hash, filename) 102 | 103 | item['ipfsHash'] = ipfs_hash 104 | item['URI'] = token_uri 105 | dictWriter.writerow(item) 106 | fileWriter.close() 107 | os.remove(f'{filename}.json') 108 | print(f'Token URI is {token_uri}') 109 | return token_uri 110 | 111 | else: 112 | print(f"{filename} upload failed.") 113 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from telegram.ext import ( 3 | Updater, 4 | CommandHandler, 5 | MessageHandler, 6 | Filters, 7 | ConversationHandler, 8 | CallbackContext, 9 | ) 10 | from uuid import uuid4 11 | from telegram import ReplyKeyboardMarkup, Update 12 | import logging 13 | import os 14 | import re 15 | from dotenv import load_dotenv 16 | from helpers.predict_price import predict 17 | from helpers.create_URI import create_URI 18 | from helpers.helpers import user_info_dict, property_info_dict, facts_to_str, \ 19 | get_owner_data, get_property_data 20 | from helpers.functions import MintNFT, RegisterProperty, RegisterOwner 21 | from vars import URL, REGISTRY_ADDRESS, NFT_ADDRESS, REGISTRY_ABI, NFT_ABI 22 | 23 | load_dotenv() 24 | 25 | BOT_TOKEN = os.environ.get('BOT_TOKEN') 26 | 27 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 28 | level=logging.INFO) 29 | logger = logging.getLogger(__name__) 30 | logger.info('Starting Bot...') 31 | 32 | # TODO: 33 | # 1) add calling functions from the contracts 34 | # 2) add subscription and notoficitations 35 | # 3) add ownership check once 36 | # 4) add webhook 37 | # 5) if the property is registered, do not ask for house info when issuing nft 38 | 39 | '''States''' 40 | 41 | CHOOSING_GOAL, GETTING_NAME, CHECK_NAME, GET_DOC_TYPE, GET_DOC_NUMBER, \ 42 | GET_HOUSE_TYPE, GET_CODICE, GET_COUNTRY, GET_REGION, GET_CITY, GET_STREET, GET_BUILDING_NUMBER,\ 43 | GET_CAP, GET_HOUSE_TYPE, GET_FLOORS, GET_SIZE, REQUEST_ROOMS, REQUEST_SURFACE, \ 44 | REQUEST_FLOORS, ESTIMATE_PRICE, CLOSING, CHOOSE_ACTION, \ 45 | NFT_DONE, GOT_WALLET, OWNER_REGISTERED, CHECKED_USER_INFO, CHECKED_PROPERTY_INFO, \ 46 | REQUEST_WALLET_ADDRESS_FOR_PROPERTY, REQUEST_WALLET_ADDRESS_FOR_OWNER = range(29) 47 | 48 | '''Bot functions''' 49 | 50 | def start(update: Update, context: CallbackContext) -> int: 51 | 52 | text = update.message.text.encode('utf-8').decode() 53 | 54 | reply_keyboard = [['Estimate house price', 'Register owner'], 55 | ['Register Property', 'Issue NFT']] 56 | 57 | # [['Issue NFT', 'Buy/sell property'], 58 | # ['Subscribe to ownership check','Check ownership once']] 59 | 60 | logger.info(f'User texted {text}') 61 | 62 | bot_welcome = "Hello! This is blockchain-based land registry. What would you like to do?" 63 | if (text.lower() == 'start again')|(text.lower() == 'cancel'): 64 | update.message.reply_text("Let's start again. What would you like to do?", 65 | reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 66 | one_time_keyboard=True)) 67 | else: 68 | update.message.reply_text(bot_welcome, 69 | reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 70 | one_time_keyboard=True)) 71 | return CHOOSING_GOAL 72 | 73 | def force_choosing_goal(update: Update, context: CallbackContext) -> int: 74 | 75 | reply_keyboard = [['Estimate house price', 'Register owner'], 76 | ['Register Property', 'Issue NFT']] 77 | 78 | text = update.message.text 79 | logger.info(f'User texted {text}, user_data: {context.user_data.items()}') 80 | update.message.reply_text('Please choose one of the following options', reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 81 | one_time_keyboard=True)) 82 | 83 | return CHOOSING_GOAL 84 | 85 | def get_name_surname(update: Update, context: CallbackContext) -> int: 86 | 87 | text = update.message.text 88 | if text not in ['Estimate house price', 'Register owner']: 89 | return CHOOSING_GOAL 90 | else: 91 | context.user_data['chosen action'] = text 92 | update.message.reply_text(f'Perfect! In order to {text.lower()}, we need to know more about you. Please type your name and surname.') 93 | 94 | return GETTING_NAME 95 | 96 | def check_name(update: Update, context: CallbackContext) -> int: 97 | 98 | text = update.message.text 99 | if len(text.split()) == 2: 100 | logger.info(f'User texted {text}') 101 | context.user_data['First name'] = text.split(' ')[0] 102 | context.user_data['Last name'] = text.split(' ')[1] 103 | update.message.reply_text(f"Your first name is {text.split(' ')[0]} and your last name is {text.split(' ')[1]}, is that correct?") 104 | return CHECK_NAME 105 | else: 106 | update.message.reply_text('Please provide correct name and surname') 107 | logger.info(f'User texted {text}, length is {len(text.split())}') 108 | return GETTING_NAME 109 | 110 | def get_doc_type(update: Update, context: CallbackContext) -> int: 111 | 112 | reply_keyboard = [['Passport', 'ID card']] 113 | text = update.message.text 114 | logger.info(f'User texted {update.message.text}') 115 | if text not in ['Yes', 'yes', 'No', 'no']: 116 | context.user_data['First name'] = text.split(' ')[0] 117 | context.user_data['Last name'] = text.split(' ')[1] 118 | update.message.reply_text('Perfect! What document are you providing?', reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 119 | one_time_keyboard=True)) 120 | 121 | return GET_DOC_TYPE 122 | 123 | def force_choosing_doc_type(update: Update, context: CallbackContext) -> int: 124 | 125 | reply_keyboard = [['Passport', 'ID card']] 126 | text = update.message.text 127 | logger.info(f'User texted {text}') 128 | update.message.reply_text('Please choose one of the following options:', 129 | reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 130 | one_time_keyboard=True)) 131 | 132 | return GET_DOC_TYPE 133 | 134 | def get_doc_number(update: Update, context: CallbackContext) -> int: 135 | 136 | text = update.message.text 137 | context.user_data['Doc type'] = text 138 | logger.info(f"User's document type is {text}") 139 | update.message.reply_text('Please type your document number.') 140 | 141 | return GET_DOC_NUMBER 142 | 143 | def get_codice(update: Update, context: CallbackContext) -> int: 144 | 145 | text = update.message.text 146 | if re.findall('[^0-9]+', text): 147 | update.message.reply_text('Please provide a valid document number') 148 | return GET_DOC_NUMBER 149 | else: 150 | context.user_data['Doc number'] = text 151 | logger.info(f"User's document number is {text}") 152 | update.message.reply_text('Now please type your fiscal code.') 153 | 154 | return GET_CODICE 155 | 156 | def get_country(update: Update, context: CallbackContext) -> int: 157 | 158 | text = str(update.message.text).strip() 159 | logger.info(f"User typed {text}") 160 | if text == 'Start again': 161 | update.message.reply_text("Let's start again") 162 | else: 163 | update.message.reply_text("Thank you! Now we need to know more about your property. Let's start with the address. What is the country?") 164 | 165 | return GET_COUNTRY 166 | 167 | def get_region(update: Update, context: CallbackContext) -> int: 168 | 169 | text = update.message.text 170 | if re.findall('[^A-Za-z\s]+', text): 171 | update.message.reply_text('Please provide a valid country name.') 172 | return GET_COUNTRY 173 | else: 174 | context.user_data['Country'] = text 175 | logger.info(f"User's country is {text}") 176 | update.message.reply_text('What is the region?') 177 | return GET_REGION 178 | 179 | def get_city(update: Update, context: CallbackContext) -> int: 180 | 181 | text = update.message.text 182 | if re.findall('[^A-Za-z\s]+', text): 183 | update.message.reply_text('Please provide a valid region name.') 184 | return GET_REGION 185 | else: 186 | context.user_data['Region'] = text 187 | logger.info(f"User's region is {text}") 188 | update.message.reply_text('What is the city?') 189 | return GET_CITY 190 | 191 | def get_street(update: Update, context: CallbackContext) -> int: 192 | 193 | text = update.message.text 194 | if re.findall('[^A-Za-z\s]+', text): 195 | update.message.reply_text('Please provide a valid city name') 196 | return GET_CITY 197 | else: 198 | context.user_data['City'] = text 199 | logger.info(f"User's city is {text}") 200 | update.message.reply_text('What is the street?') 201 | 202 | return GET_STREET 203 | 204 | def get_building_number(update: Update, context: CallbackContext) -> int: 205 | 206 | text = update.message.text 207 | if re.findall('[^A-Za-z\s]+', text): 208 | update.message.reply_text('Please provide a valid street name') 209 | return GET_STREET 210 | else: 211 | context.user_data['Street'] = text 212 | logger.info(f"User's street is {text}") 213 | update.message.reply_text('What is the building number?') 214 | return GET_BUILDING_NUMBER 215 | 216 | def get_cap(update: Update, context: CallbackContext) -> int: 217 | 218 | text = update.message.text 219 | if re.findall('[^0-9]+', text): 220 | update.message.reply_text('Please provide a valid building number') 221 | return GET_BUILDING_NUMBER 222 | else: 223 | context.user_data['Building number'] = text 224 | logger.info(f"User's building number is {text}") 225 | update.message.reply_text('What is the zip code?') 226 | 227 | return GET_CAP 228 | 229 | def get_house_type(update: Update, context: CallbackContext) -> int: 230 | 231 | reply_keyboard = [['Single family detached house', 'Apartment'], 232 | ['Castle', 'Chalet', 'Bungalow', 'Cave house']] 233 | text = update.message.text 234 | if re.findall('[^0-9]+', text): 235 | update.message.reply_text('Please provide a valid zip code.') 236 | return GET_CAP 237 | else: 238 | context.user_data['Cap'] = text 239 | logger.info(f"User's cap os {text}") 240 | update.message.reply_text('What is the type of the property?', reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 241 | one_time_keyboard=True)) 242 | 243 | return GET_HOUSE_TYPE 244 | 245 | def get_floors(update: Update, context: CallbackContext) -> int: 246 | 247 | text = update.message.text 248 | if text not in ['Single family detached house', 'Apartment','Castle', 'Chalet', 'Bungalow', 'Cave house']: 249 | update.message.reply_text('Please choose one of the following options') 250 | return GET_HOUSE_TYPE 251 | else: 252 | context.user_data['Property type'] = text 253 | logger.info(f"User's house type is {text}") 254 | update.message.reply_text('How many floors does the property have?') 255 | 256 | return GET_FLOORS 257 | 258 | def get_size(update: Update, context: CallbackContext) -> int: 259 | 260 | text = update.message.text 261 | if re.findall('[^0-9]+', text): 262 | update.message.reply_text('Please provide valid number') 263 | return GET_FLOORS 264 | else: 265 | context.user_data['Floors'] = text 266 | logger.info(f"User's property has {text} floors") 267 | update.message.reply_text('What is the size of the property in sq meters?') 268 | 269 | return GET_SIZE 270 | 271 | def received_property_information(update: Update, context: CallbackContext) -> int: 272 | 273 | text = update.message.text 274 | if re.findall('[^0-9]+', text): 275 | update.message.reply_text('Please provide a valid property size') 276 | return GET_SIZE 277 | else: 278 | context.user_data['Property size'] = text 279 | reply_keyboard = [['All correct', 'Start again']] 280 | update.message.reply_text( 281 | "Awesome! Thank you, now we are all set. Please check the correctness of your data:" 282 | f"{facts_to_str(context.user_data)}", reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 283 | one_time_keyboard=True)) 284 | 285 | return CHECKED_PROPERTY_INFO 286 | 287 | def received_user_information(update: Update, context: CallbackContext) -> int: 288 | 289 | text = update.message.text 290 | context.user_data['wallet address'] = text 291 | # context.user_data['id'] = str(uuid4()) 292 | reply_keyboard = [['All correct', 'Start again']] 293 | update.message.reply_text( 294 | "Awesome! Thank you, now we are all set. Please check the correctness of your data:" 295 | f"{facts_to_str(context.user_data)}", reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 296 | one_time_keyboard=True)) 297 | 298 | return CHECKED_USER_INFO 299 | 300 | def request_wallet_address_for_owner(update: Update, context: CallbackContext) -> int: 301 | 302 | text = str(update.message.text).strip() 303 | if re.findall('[^a-zA-Z0-9]+', text): 304 | update.message.reply_text('Please provide a valid fiscal code.') 305 | return GET_DOC_NUMBER 306 | else: 307 | context.user_data['Fiscal code'] = text 308 | logger.info(f"User's fiscal code is {text}") 309 | update.message.reply_text('Please provide your wallet address') 310 | 311 | return REQUEST_WALLET_ADDRESS_FOR_OWNER 312 | 313 | def request_wallet_address_for_property(update: Update, context: CallbackContext) -> int: 314 | 315 | text = str(update.message.text).strip() 316 | if re.findall('[^0-9]+', text): 317 | update.message.reply_text('Please provide a valid property size') 318 | return GET_SIZE 319 | else: 320 | context.user_data['Property size'] = text 321 | logger.info(f"User's property size is {text}") 322 | update.message.reply_text('Please provide your wallet address') 323 | 324 | return REQUEST_WALLET_ADDRESS_FOR_OWNER 325 | 326 | def store_wallet_address(update: Update, context: CallbackContext) -> int: 327 | 328 | text = update.message.text 329 | logger.info(f"User wallet address - {text}") 330 | context.user_data['wallet address'] = text 331 | 332 | reply_keyboard = [['All correct, register me', 'Start again'], 333 | ['All correct, register property', 'All correct, issue nft']] 334 | update.message.reply_text( 335 | "Awesome! Thank you, now we are all set. Please check the correctness of your data:" 336 | f"{facts_to_str(context.user_data)}", reply_markup=ReplyKeyboardMarkup(reply_keyboard, resize_keyboard=True, 337 | one_time_keyboard=True)) 338 | 339 | return GOT_WALLET 340 | 341 | def register_property(update: Update, context: CallbackContext) -> int: 342 | 343 | property_data = get_property_data(context) 344 | RegisterProperty(URL, REGISTRY_ADDRESS, REGISTRY_ABI, *property_data) 345 | update.message.reply_text("Your property has been registered. Type /start to return to the beginning") 346 | 347 | return CHOOSE_ACTION 348 | 349 | def register_owner(update: Update, context: CallbackContext) -> int: 350 | 351 | text = update.message.text 352 | owner_data = get_owner_data(context) 353 | logger.info(f'the user texted {text}, owner data is: {owner_data}') 354 | RegisterOwner(URL, REGISTRY_ADDRESS, REGISTRY_ABI, *owner_data) 355 | logger.info('Owner is registered') 356 | update.message.reply_text( 357 | f'Awesome! You are successfully registered. In order to register a property, please deposit money to the address: {REGISTRY_ADDRESS}. Type "done" when payment is completed.', 358 | ) 359 | 360 | return OWNER_REGISTERED 361 | 362 | def require_payment(update: Update, context: CallbackContext) -> int: 363 | 364 | text = update.message.text 365 | logger.info(f"User typed {text}") 366 | update.message.reply_text(f'In order to proceed please deposit money to the address: {REGISTRY_ADDRESS}.Type "done" when payment is completed.') 367 | 368 | return OWNER_REGISTERED 369 | 370 | def mint_nft(update: Update, context: CallbackContext) -> int: 371 | 372 | property_data = get_property_data(context)[1:] 373 | token_uri = create_URI(*property_data) 374 | owner_address = context.user_data['wallet address'] 375 | logger.info(f"Token uri is {token_uri}") 376 | 377 | MintNFT(URL, NFT_ADDRESS, NFT_ABI, token_uri, owner_address) 378 | logger.info(f"Token uri is {token_uri}") 379 | update.message.reply_text(f'Your NFT is successfully issued. Your token URI is {token_uri}') 380 | 381 | return NFT_DONE 382 | 383 | def request_house_surface(update: Update, context: CallbackContext) -> int: 384 | 385 | update.message.reply_text('How much is the surface of the property?') 386 | 387 | return REQUEST_SURFACE 388 | 389 | def request_house_floors(update: Update, context: CallbackContext) -> int: 390 | 391 | text = update.message.text 392 | if re.findall('[^0-9]+', text): 393 | update.message.reply_text('Please provide a valid surface size') 394 | return REQUEST_SURFACE 395 | else: 396 | logger.info(f"House surface is {text}") 397 | context.user_data['Surface'] = text 398 | update.message.reply_text('Which floor is it?') 399 | 400 | return REQUEST_FLOORS 401 | 402 | def request_house_rooms(update: Update, context: CallbackContext) -> int: 403 | 404 | text = update.message.text 405 | if re.findall('[^0-9]+', text): 406 | update.message.reply_text('Please provide a valid floor') 407 | return REQUEST_FLOORS 408 | 409 | else: 410 | context.user_data['Floor'] = text 411 | update.message.reply_text('How many rooms are there?') 412 | return REQUEST_ROOMS 413 | 414 | def estimate_price(update: Update, context: CallbackContext) -> int: 415 | 416 | text = update.message.text 417 | if re.findall('[^0-9]+', text): 418 | update.message.reply_text('Please provide a valid number') 419 | 420 | return REQUEST_ROOMS 421 | 422 | else: 423 | context.user_data['Rooms'] = text 424 | surface, rooms, floor = context.user_data['Surface'], context.user_data['Rooms'] ,context.user_data['Floor'] 425 | estimation = predict(surface, rooms, floor) 426 | logger.info(f'Estimated price is {estimation}') 427 | update.message.reply_text(f"The estimated price of the house is {estimation}. Please type '/start' to go to the beginning") 428 | 429 | return ESTIMATE_PRICE 430 | 431 | def close_conv(update: Update, context: CallbackContext) -> int: 432 | 433 | update.message.reply_text("Thank you! Our team will check all the information provided and will come back to you soon.") 434 | 435 | return CHOOSING_GOAL 436 | 437 | def main() -> None: 438 | """Run the bot""" 439 | 440 | updater = Updater(BOT_TOKEN) 441 | dispatcher = updater.dispatcher 442 | 443 | conv_handler = ConversationHandler( 444 | entry_points=[CommandHandler('start', start), MessageHandler(Filters.text('cancel'), start)], 445 | states={ 446 | CHOOSING_GOAL: [ 447 | MessageHandler(Filters.text('Estimate house price'), request_house_surface), 448 | MessageHandler(Filters.text(['Register owner']), get_name_surname 449 | ), 450 | MessageHandler(Filters.text(['Register Property']), get_country), 451 | MessageHandler(Filters.text(['Issue NFT']), get_country), 452 | MessageHandler(Filters.text & ~Filters.text(['Register owner', 'Estimate house price', 'Register Property', 'Issue NFT']), force_choosing_goal) 453 | ], 454 | REQUEST_SURFACE: [ 455 | MessageHandler(Filters.text, request_house_floors) 456 | ], 457 | REQUEST_FLOORS: [ 458 | MessageHandler(Filters.text, request_house_rooms) 459 | ], 460 | REQUEST_ROOMS: [ 461 | MessageHandler(Filters.text, estimate_price) 462 | ], 463 | ESTIMATE_PRICE: [ 464 | MessageHandler(Filters.text, start) 465 | ], 466 | REQUEST_WALLET_ADDRESS_FOR_OWNER: [ 467 | MessageHandler(Filters.text, store_wallet_address) 468 | ], 469 | REQUEST_WALLET_ADDRESS_FOR_PROPERTY: [ 470 | MessageHandler(Filters.text, store_wallet_address) 471 | ], 472 | GOT_WALLET: [ 473 | MessageHandler(Filters.text(['All correct, register me']), register_owner), 474 | MessageHandler(Filters.text(['All correct, register property']), register_property), 475 | MessageHandler(Filters.text(['All correct, issue nft']), mint_nft), 476 | MessageHandler(~Filters.text(['All correct']), start), 477 | ], 478 | OWNER_REGISTERED: [ 479 | MessageHandler(Filters.text(['done', 'Done']), get_country), 480 | MessageHandler(~Filters.text(['done', 'Done']), require_payment) 481 | ], 482 | GETTING_NAME: [ 483 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), check_name), 484 | MessageHandler(Filters.text(['back', 'Back']), get_name_surname) 485 | ], 486 | CHECK_NAME: [ 487 | MessageHandler(Filters.text(['yes', 'Yes']), get_doc_type), 488 | MessageHandler(Filters.text(['no', 'No']), get_name_surname), 489 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back', 'yes', 'Yes', 'no', 'No']), get_name_surname), 490 | MessageHandler(Filters.text(['back', 'Back']), get_name_surname) 491 | ], 492 | GET_DOC_TYPE: [ 493 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']),get_doc_number), 494 | MessageHandler(Filters.text(['back', 'Back']), get_doc_type) 495 | ], 496 | GET_DOC_NUMBER: [ 497 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']),get_codice), 498 | MessageHandler(Filters.text(['back', 'Back']), get_doc_number) 499 | ], 500 | GET_CODICE: [ 501 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), request_wallet_address_for_owner), 502 | MessageHandler(Filters.text(['back', 'Back']), get_codice) 503 | ], 504 | GET_COUNTRY: [ 505 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_region), 506 | MessageHandler(Filters.text(['back', 'Back']), get_country) 507 | ], 508 | GET_REGION: [ 509 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_city), 510 | MessageHandler(Filters.text(['back', 'Back']), get_region) 511 | ], 512 | GET_CITY: [ 513 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_street), 514 | MessageHandler(Filters.text(['back', 'Back']), get_city) 515 | ], 516 | GET_STREET: [ 517 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_building_number), 518 | MessageHandler(Filters.text(['back', 'Back']),get_street) 519 | ], 520 | GET_BUILDING_NUMBER: [ 521 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_cap), 522 | MessageHandler(Filters.text(['back', 'Back']), get_building_number) 523 | ], 524 | GET_CAP: [ 525 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_house_type), 526 | MessageHandler(Filters.text(['back', 'Back']), get_cap) 527 | ], 528 | GET_HOUSE_TYPE: [ 529 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_floors), 530 | MessageHandler(Filters.text(['back', 'Back']), get_house_type) 531 | ], 532 | GET_FLOORS: [ 533 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), get_size), 534 | MessageHandler(Filters.text(['back', 'Back']), get_floors) 535 | ], 536 | GET_SIZE: [ 537 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), request_wallet_address_for_property), 538 | MessageHandler(Filters.text(['back', 'Back']), get_size) 539 | ], 540 | CHECKED_USER_INFO: [ 541 | MessageHandler(Filters.text('All correct'), register_owner), 542 | MessageHandler(Filters.text('Start again'), get_country) 543 | ], 544 | CHECKED_PROPERTY_INFO: [ 545 | MessageHandler(Filters.text('All correct'), register_property), 546 | MessageHandler(Filters.text('Start again'), get_country) 547 | ], 548 | CHOOSE_ACTION: [ 549 | MessageHandler(Filters.text('Issue NFT'), mint_nft) 550 | ], 551 | NFT_DONE: [ 552 | MessageHandler(Filters.text, close_conv) 553 | ], 554 | CLOSING: [ 555 | MessageHandler(Filters.text & ~Filters.text(['back', 'Back']), close_conv), 556 | MessageHandler(Filters.text(['back', 'Back']), received_user_information) 557 | ] 558 | }, 559 | fallbacks = [MessageHandler(Filters.text(['cancel', '/start']), start)] 560 | ) 561 | 562 | dispatcher.add_handler(conv_handler) 563 | 564 | # updater.start_webhook(listen='0.0.0.0', 565 | # port=PORT, 566 | # # key='/Users/aliyadavletshina/private.key', 567 | # # cert='/Users/aliyadavletshina/cert.pem', 568 | # url_path=BOT_TOKEN, 569 | # webhook_url = APP_URL + BOT_TOKEN) 570 | updater.start_polling() 571 | updater.idle() 572 | 573 | if __name__ == '__main__': 574 | main() 575 | 576 | --------------------------------------------------------------------------------