├── .gitignore ├── README.md ├── mixin_api.py ├── mixin_config.default.py ├── mixin_config.py ├── mixin_msg_test.py ├── mixin_ws_api.py ├── test_api.py └── ws_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mixin Python3 SDK 2 | - All methods implement on https://developers.mixin.one/api 3 | - This SDK support Python3.x 4 | - Tutorial Video in https://www.youtube.com/playlist?list=PLMt8rZaHF-0Yj0w6tHeD1vxBA7f7GU-0- 5 | 6 | ## mixin_api.py 7 | - This SDK base on https://github.com/myrual/mixin_client_demo/blob/master/mixin_api.py 8 | - You can see Demo in test_api.py 9 | 10 | ## mixin_ws_api.py 11 | - This SDK base on https://github.com/myrual/mixin_client_demo/blob/master/home_of_cnb_robot.py 12 | - You can see Demo in ws_test.py -------------------------------------------------------------------------------- /mixin_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Mixin API for Python 3.x 4 | This SDK base on 'https://github.com/myrual/mixin_client_demo/blob/master/mixin_api.py' 5 | some method note '?', because can't run right result, may be it will be resolved later. 6 | 7 | env: python 3.x 8 | code by lee.c 9 | update at 2018.12.2 10 | """ 11 | 12 | from Crypto.PublicKey import RSA 13 | import base64 14 | from Crypto.Cipher import PKCS1_OAEP 15 | from Crypto.Signature import PKCS1_v1_5 16 | import Crypto 17 | import time 18 | from Crypto import Random 19 | from Crypto.Cipher import AES 20 | import hashlib 21 | import datetime 22 | import jwt 23 | import uuid 24 | import json 25 | import requests 26 | from urllib.parse import urlencode 27 | 28 | 29 | class MIXIN_API: 30 | def __init__(self, mixin_config): 31 | 32 | # robot's config 33 | self.client_id = mixin_config.client_id 34 | self.client_secret = mixin_config.client_secret 35 | self.pay_session_id = mixin_config.pay_session_id 36 | self.pay_pin = mixin_config.pay_pin 37 | self.pin_token = mixin_config.pin_token 38 | self.private_key = mixin_config.private_key 39 | 40 | 41 | self.keyForAES = "" 42 | # mixin api base url 43 | self.api_base_url = 'https://api.mixin.one' 44 | 45 | """ 46 | BASE METHON 47 | """ 48 | 49 | def generateSig(self, method, uri, body): 50 | hashresult = hashlib.sha256((method + uri+body).encode('utf-8')).hexdigest() 51 | return hashresult 52 | 53 | def genGETPOSTSig(self, methodstring, uristring, bodystring): 54 | jwtSig = self.generateSig(methodstring, uristring, bodystring) 55 | 56 | return jwtSig 57 | 58 | 59 | def genGETSig(self, uristring, bodystring): 60 | return self.genGETPOSTSig("GET", uristring, bodystring) 61 | 62 | def genPOSTSig(self, uristring, bodystring): 63 | return self.genGETPOSTSig("POST", uristring, bodystring) 64 | 65 | def genGETJwtToken(self, uristring, bodystring, jti): 66 | jwtSig = self.genGETSig(uristring, bodystring) 67 | iat = datetime.datetime.utcnow() 68 | exp = datetime.datetime.utcnow() + datetime.timedelta(seconds=200) 69 | encoded = jwt.encode({'uid':self.client_id, 'sid':self.pay_session_id,'iat':iat,'exp': exp, 'jti':jti,'sig':jwtSig}, self.private_key, algorithm='RS512') 70 | 71 | return encoded 72 | 73 | def genGETListenSignedToken(self, uristring, bodystring, jti): 74 | jwtSig = self.genGETSig(uristring, bodystring) 75 | iat = datetime.datetime.utcnow() 76 | exp = datetime.datetime.utcnow() + datetime.timedelta(seconds=200) 77 | encoded = jwt.encode({'uid':self.client_id, 'sid':self.pay_session_id,'iat':iat,'exp': exp, 'jti':jti,'sig':jwtSig}, self.private_key, algorithm='RS512') 78 | privKeyObj = RSA.importKey(self.private_key) 79 | signer = PKCS1_v1_5.new(privKeyObj) 80 | signature = signer.sign(encoded) 81 | return signature 82 | 83 | 84 | def genPOSTJwtToken(self, uristring, bodystring, jti): 85 | jwtSig = self.genPOSTSig(uristring, bodystring) 86 | iat = datetime.datetime.utcnow() 87 | exp = datetime.datetime.utcnow() + datetime.timedelta(seconds=200) 88 | encoded = jwt.encode({'uid':self.client_id, 'sid':self.pay_session_id,'iat':iat,'exp': exp, 'jti':jti,'sig':jwtSig}, self.private_key, algorithm='RS512') 89 | return encoded 90 | 91 | def genEncrypedPin(self, iterString = None): 92 | if self.keyForAES == "": 93 | privKeyObj = RSA.importKey(self.private_key) 94 | 95 | decoded_result = base64.b64decode(self.pin_token) 96 | 97 | cipher = PKCS1_OAEP.new(key=privKeyObj, hashAlgo=Crypto.Hash.SHA256, label=self.pay_session_id.encode("utf-8")) 98 | 99 | decrypted_msg = cipher.decrypt(decoded_result) 100 | 101 | self.keyForAES = decrypted_msg 102 | 103 | ts = int(time.time()) 104 | tszero = ts % 0x100 105 | tsone = (ts % 0x10000) >> 8 106 | tstwo = (ts % 0x1000000) >> 16 107 | tsthree = (ts % 0x100000000) >> 24 108 | 109 | 110 | tszero = chr(tszero).encode('latin1').decode('latin1') 111 | tsone = chr(tsone) 112 | tstwo = chr(tstwo) 113 | tsthree = chr(tsthree) 114 | 115 | tsstring = tszero + tsone + tstwo + tsthree + '\0\0\0\0' 116 | if iterString is None: 117 | ts = int(time.time() * 1000000) 118 | tszero = ts % 0x100 119 | tsone = (ts % 0x10000) >> 8 120 | tstwo = (ts % 0x1000000) >> 16 121 | tsthree = (ts % 0x100000000) >> 24 122 | tsfour = (ts % 0x10000000000) >> 32 123 | tsfive = (ts % 0x1000000000000) >> 40 124 | tssix = (ts % 0x100000000000000) >> 48 125 | tsseven = (ts % 0x10000000000000000) >> 56 126 | 127 | tszero = chr(tszero).encode('latin1').decode('latin1') 128 | tsone = chr(tsone) 129 | tstwo = chr(tstwo) 130 | tsthree = chr(tsthree) 131 | tsfour = chr(tsfour) 132 | tsfive= chr(tsfive) 133 | tssix = chr(tssix) 134 | tsseven = chr(tsseven) 135 | iterStringByTS = tszero + tsone + tstwo + tsthree + tsfour + tsfive + tssix + tsseven 136 | 137 | toEncryptContent = self.pay_pin + tsstring + iterStringByTS 138 | else: 139 | toEncryptContent = self.pay_pin + tsstring + iterString 140 | 141 | lenOfToEncryptContent = len(toEncryptContent) 142 | toPadCount = 16 - lenOfToEncryptContent % 16 143 | if toPadCount > 0: 144 | paddedContent = toEncryptContent + chr(toPadCount) * toPadCount 145 | else: 146 | paddedContent = toEncryptContent 147 | 148 | iv = Random.new().read(AES.block_size) 149 | 150 | 151 | cipher = AES.new(self.keyForAES, AES.MODE_CBC,iv) 152 | encrypted_result = cipher.encrypt(paddedContent.encode('latin1')) 153 | 154 | msg = iv + encrypted_result 155 | encrypted_pin = base64.b64encode(msg) 156 | 157 | return encrypted_pin 158 | 159 | """ 160 | COMMON METHON 161 | """ 162 | 163 | """ 164 | generate API url 165 | """ 166 | def __genUrl(self, path): 167 | return self.api_base_url + path 168 | 169 | """ 170 | generate GET http request 171 | """ 172 | def __genGetRequest(self, path, auth_token=""): 173 | 174 | url = self.__genUrl(path) 175 | 176 | if auth_token == "": 177 | r = requests.get(url) 178 | else: 179 | r = requests.get(url, headers={"Authorization": "Bearer " + auth_token}) 180 | 181 | result_obj = r.json() 182 | print(result_obj) 183 | return result_obj['data'] 184 | 185 | """ 186 | generate POST http request 187 | """ 188 | def __genPostRequest(self, path, body, auth_token=""): 189 | 190 | # generate url 191 | url = self.__genUrl(path) 192 | 193 | # transfer obj => json string 194 | body_in_json = json.dumps(body) 195 | 196 | if auth_token == "": 197 | r = requests.post(url, json=body_in_json) 198 | else: 199 | r = requests.post(url, json=body_in_json, headers={"Authorization": "Bearer " + auth_token}) 200 | 201 | result_obj = r.json() 202 | print(result_obj) 203 | return result_obj 204 | 205 | """ 206 | generate Mixin Network GET http request 207 | """ 208 | def __genNetworkGetRequest(self, path, body=None, auth_token=""): 209 | 210 | url = self.__genUrl(path) 211 | 212 | if body is not None: 213 | body = urlencode(body) 214 | else: 215 | body = "" 216 | 217 | if auth_token == "": 218 | token = self.genGETJwtToken(path, body, str(uuid.uuid4())) 219 | auth_token = token.decode('utf8') 220 | 221 | r = requests.get(url, headers={"Authorization": "Bearer " + auth_token}) 222 | result_obj = r.json() 223 | return result_obj 224 | 225 | 226 | """ 227 | generate Mixin Network POST http request 228 | """ 229 | # TODO: request 230 | def __genNetworkPostRequest(self, path, body, auth_token=""): 231 | 232 | # transfer obj => json string 233 | body_in_json = json.dumps(body) 234 | 235 | # generate robot's auth token 236 | if auth_token == "": 237 | token = self.genPOSTJwtToken(path, body_in_json, str(uuid.uuid4())) 238 | auth_token = token.decode('utf8') 239 | headers = { 240 | 'Content-Type' : 'application/json', 241 | 'Authorization' : 'Bearer ' + auth_token, 242 | } 243 | # generate url 244 | url = self.__genUrl(path) 245 | 246 | r = requests.post(url, json=body, headers=headers) 247 | # {'error': {'status': 202, 'code': 20118, 'description': 'Invalid PIN format.'}} 248 | 249 | # r = requests.post(url, data=body, headers=headers) 250 | # {'error': {'status': 202, 'code': 401, 'description': 'Unauthorized, maybe invalid token.'}} 251 | result_obj = r.json() 252 | print(result_obj) 253 | return result_obj 254 | 255 | """ 256 | ============ 257 | MESSENGER PRIVATE APIs 258 | ============ 259 | auth token need request 'https://api.mixin.one/me' to get. 260 | """ 261 | 262 | 263 | """ 264 | Read user's all assets. 265 | """ 266 | def getMyAssets(self, auth_token=""): 267 | 268 | return self.__genGetRequest('/assets', auth_token) 269 | 270 | """ 271 | Read self profile. 272 | """ 273 | def getMyProfile(self, auth_token): 274 | return self.__genGetRequest('/me', auth_token) 275 | 276 | """ 277 | ? 278 | Update my preferences. 279 | """ 280 | def updateMyPerference(self,receive_message_source="EVERYBODY",accept_conversation_source="EVERYBODY"): 281 | 282 | body = { 283 | "receive_message_source": receive_message_source, 284 | "accept_conversation_source": accept_conversation_source 285 | } 286 | 287 | return self.__genPostRequest('/me/preferences', body) 288 | 289 | 290 | """ 291 | ? 292 | Update my profile. 293 | """ 294 | def updateMyProfile(self, full_name, auth_token, avatar_base64=""): 295 | 296 | body = { 297 | "full_name": full_name, 298 | "avatar_base64": avatar_base64 299 | } 300 | 301 | return self.__genPostRequest('/me', body, auth_token) 302 | 303 | """ 304 | Get users information by IDs. 305 | """ 306 | def getUsersInfo(self, user_ids, auth_token): 307 | return self.__genPostRequest('/users/fetch', user_ids, auth_token) 308 | 309 | """ 310 | Get user's information by ID. 311 | """ 312 | def getUserInfo(self, user_id, auth_token): 313 | return self.__genGetRequest('/users/' + user_id, auth_token) 314 | 315 | """ 316 | Search user by Mixin ID or Phone Number. 317 | """ 318 | def SearchUser(self, q, auth_token=""): 319 | return self.__genGetRequest('/search/' + q, auth_token) 320 | 321 | """ 322 | Rotate user’s code_id. 323 | """ 324 | def rotateUserQR(self, auth_token): 325 | return self.__genGetRequest('/me/code', auth_token) 326 | 327 | """ 328 | Get my friends. 329 | """ 330 | def getMyFriends(self, auth_token): 331 | return self.__genGetRequest('/friends', auth_token) 332 | 333 | """ 334 | Create a GROUP or CONTACT conversation. 335 | """ 336 | def createConv(self, category, conversation_id, participants, action, role, user_id, auth_token): 337 | 338 | body = { 339 | "category": category, 340 | "conversation_id": conversation_id, 341 | "participants": participants, 342 | "action": action, 343 | "role": role, 344 | "user_id": user_id 345 | } 346 | 347 | return self.__genPostRequest('/conversations', body, auth_token) 348 | 349 | """ 350 | Read conversation by conversation_id. 351 | """ 352 | def getConv(self, conversation_id, auth_token): 353 | return self.__genGetRequest('/conversations/' + conversation_id, auth_token) 354 | 355 | 356 | """ 357 | ============ 358 | NETWORK PRIVATE APIs 359 | ============ 360 | auth token need robot related param to generate. 361 | """ 362 | 363 | """ 364 | PIN is used to manage user’s addresses, assets and etc. There’s no default PIN for a Mixin Network user (except APP). 365 | if auth_token is empty, it create robot' pin. 366 | if auth_token is set, it create messenger user pin. 367 | """ 368 | def updatePin(self, new_pin, old_pin, auth_token=""): 369 | old_inside_pay_pin = self.pay_pin 370 | self.pay_pin = new_pin 371 | newEncrypedPin = self.genEncrypedPin() 372 | if old_pin == "": 373 | body = { 374 | "old_pin": "", 375 | "pin": newEncrypedPin.decode() 376 | } 377 | else: 378 | 379 | self.pay_pin = old_pin 380 | oldEncryptedPin = self.genEncrypedPin() 381 | body = { 382 | "old_pin": oldEncryptedPin.decode(), 383 | "pin": newEncrypedPin.decode() 384 | } 385 | self.pay_pin = old_inside_pay_pin 386 | return self.__genNetworkPostRequest('/pin/update', body, auth_token) 387 | 388 | """ 389 | Verify PIN if is valid or not. For example, you can verify PIN before updating it. 390 | if auth_token is empty, it verify robot' pin. 391 | if auth_token is set, it verify messenger user pin. 392 | """ 393 | def verifyPin(self, auth_token=""): 394 | enPin = self.genEncrypedPin() 395 | body = { 396 | "pin": enPin.decode() 397 | } 398 | 399 | return self.__genNetworkPostRequest('/pin/verify', body, auth_token) 400 | 401 | """ 402 | Grant an asset's deposit address, usually it is public_key, but account_name and account_tag is used for EOS. 403 | """ 404 | def deposit(self, asset_id): 405 | return self.__genNetworkGetRequest(' /assets/' + asset_id) 406 | 407 | 408 | """ 409 | withdrawals robot asset to address_id 410 | Tips:Get assets out of Mixin Network, neet to create an address for withdrawal. 411 | """ 412 | def withdrawals(self, address_id, amount, memo, trace_id=""): 413 | encrypted_pin = self.genEncrypedPin() 414 | 415 | if trace_id == "": 416 | trace_id = str(uuid.uuid1()) 417 | 418 | body = { 419 | "address_id": address_id, 420 | "pin": encrypted_pin, 421 | "amount": amount, 422 | "trace_id": trace_id, 423 | "memo": memo 424 | 425 | } 426 | 427 | return self.__genNetworkPostRequest('/withdrawals/', body) 428 | 429 | 430 | """ 431 | Create an address for withdrawal, you can only withdraw through an existent address. 432 | """ 433 | def createAddress(self, asset_id, public_key = "", label = "", account_name = "", account_tag = ""): 434 | 435 | body = { 436 | "asset_id": asset_id, 437 | "pin": self.genEncrypedPin().decode(), 438 | "public_key": public_key, 439 | "label": label, 440 | "account_name": account_name, 441 | "account_tag": account_tag, 442 | } 443 | print(body) 444 | return self.__genNetworkPostRequest('/addresses', body) 445 | 446 | 447 | """ 448 | Delete an address by ID. 449 | """ 450 | def delAddress(self, address_id): 451 | 452 | encrypted_pin = self.genEncrypedPin() 453 | 454 | body = {"pin": encrypted_pin} 455 | 456 | return self.__genNetworkPostRequest('/addresses/' + address_id + '/delete', body) 457 | 458 | 459 | """ 460 | Read an address by ID. 461 | """ 462 | def getAddress(self, address_id): 463 | return self.__genNetworkGetRequest('/addresses' + address_id) 464 | 465 | """ 466 | Transfer of assets between Mixin Network users. 467 | """ 468 | def transferTo(self, to_user_id, to_asset_id, to_asset_amount, memo, trace_uuid=""): 469 | 470 | # generate encrypted pin 471 | encrypted_pin = self.genEncrypedPin() 472 | 473 | body = {'asset_id': to_asset_id, 'counter_user_id': to_user_id, 'amount': str(to_asset_amount), 474 | 'pin': encrypted_pin.decode('utf8'), 'trace_id': trace_uuid, 'memo': memo} 475 | if trace_uuid == "": 476 | body['trace_id'] = str(uuid.uuid1()) 477 | 478 | return self.__genNetworkPostRequest('/transfers', body) 479 | 480 | """ 481 | Read transfer by trace ID. 482 | """ 483 | def getTransfer(self, trace_id): 484 | return self.__genNetworkGetRequest('/transfers/trace/' + trace_id) 485 | 486 | """ 487 | Verify a transfer, payment status if it is 'paid' or 'pending'. 488 | """ 489 | def verifyPayment(self, asset_id, opponent_id, amount, trace_id): 490 | 491 | body = { 492 | "asset_id": asset_id, 493 | "opponent_id": opponent_id, 494 | "amount": amount, 495 | "trace_id": trace_id 496 | } 497 | 498 | return self.__genNetworkPostRequest('/payments', body) 499 | 500 | """ 501 | Read asset by asset ID. 502 | """ 503 | def getAsset(self, asset_id): 504 | return self.__genNetworkGetRequest('/assets/' + asset_id) 505 | 506 | """ 507 | Read external transactions (pending deposits) by public_key and asset_id, use account_tag for EOS. 508 | """ 509 | def extTrans(self, asset_id, public_key, account_tag, account_name, limit, offset): 510 | 511 | body = { 512 | "asset": asset_id, 513 | "public_key": public_key, 514 | "account_tag": account_tag, 515 | "account_name": account_name, 516 | "limit": limit, 517 | "offset": offset 518 | } 519 | 520 | return self.__genNetworkGetRequest('/external/transactions', body) 521 | 522 | 523 | """ 524 | Create a new Mixin Network user (like a normal Mixin Messenger user). You should keep PrivateKey which is used to sign an AuthenticationToken and encrypted PIN for the user. 525 | """ 526 | def createUser(self, session_secret, full_name): 527 | 528 | body = { 529 | "session_secret": session_secret, 530 | "full_name": full_name 531 | } 532 | 533 | return self.__genNetworkPostRequest('/users', body) 534 | 535 | 536 | """ 537 | =========== 538 | NETWORK PUBLIC APIs 539 | =========== 540 | """ 541 | 542 | """ 543 | Read top valuable assets of Mixin Network. 544 | """ 545 | def topAssets(self): 546 | return self.__genGetRequest('/network') 547 | 548 | """ 549 | Read public snapshots of Mixin Network. 550 | """ 551 | def snapshots(self, offset, asset_id, order='DESC',limit=100): 552 | # TODO: SET offset default(UTC TIME) 553 | body = { 554 | "limit":limit, 555 | "offset":offset, 556 | "asset":asset_id, 557 | "order":order 558 | } 559 | 560 | return self.__genGetRequest('/network/snapshots', body) 561 | 562 | 563 | """ 564 | Read public snapshots of Mixin Network by ID. 565 | """ 566 | def snapshot(self, snapshot_id): 567 | return self.__genGetRequest('/network/snapshots/' + snapshot_id) 568 | -------------------------------------------------------------------------------- /mixin_config.default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Mixin Config 4 | get below config from 'https://developers.mixin.one/dashboard' 5 | code by lee.c 6 | update at 2018.12.2 7 | """ 8 | 9 | client_id= '' 10 | client_secret = '' 11 | 12 | 13 | pay_pin = '' 14 | pay_session_id = '' 15 | pin_token = "" 16 | 17 | 18 | private_key = """-----BEGIN RSA PRIVATE KEY----- 19 | -----END RSA PRIVATE KEY-----""" 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /mixin_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Mixin Config 4 | get below config from 'https://developers.mixin.one/dashboard' 5 | code by lee.c 6 | update at 2018.12.2 7 | """ 8 | 9 | client_id= '23b554a2-34da-4c43-9e74-1dcbcd09804c' 10 | client_secret = 'a05962e74abf21fe543c4f3ed36b6d3521567eb87f992b96d5988587a1f51952' 11 | 12 | 13 | pay_pin = '397046' 14 | pay_session_id = '3fcd444e-5e45-4cfc-89b1-51cf4b3f7659' 15 | pin_token = "V6E0XHEugsG29h70qoEA4f3dDD2gcJfhLtYZDyz2QH5TibNCRWXaikKC5VoRwgGR2IrBvxdW59QvfQkCXfu94j/2FyWhTA6ICErABGhFcxOTnzQbrvFLMJLnJgLJ5GBPCwmK8sIvEnvwstLNpYkKLYpGO6R7v9eXGS7AI2mvAis=" 16 | 17 | 18 | private_key = """-----BEGIN RSA PRIVATE KEY----- 19 | MIICXAIBAAKBgQCU0YUshjp0CtOZ18qBB/lkP+saQWDOsbawCLHvzmisMmI8j7dj 20 | yRmUxhXVwrDQEieFecOw+1AlTWNyT/FKhqEuVApXEyOQ8lrPKD/QLwptw99SM/bX 21 | LHMrHNW3Jmwun5MmjEogJrHi6NMMUovI6w6LV0sn4ZBp1tZeaEjWZGoSwQIDAQAB 22 | AoGAKSTQI+IscP65R9RQSWIyAhRl5IlkwWCCuKJ+x2USrWD0pfe55R2pM+ecC9Ba 23 | 3/vU72MdxmWE3/tIXkdZ15fnIaB3FOo/bYepio/5vUKTwCIsyAAYelW5huhUohfg 24 | NLVWEE+wfrVPFx0THn04q6Y6YdsajFv/xIfS1pMe6aFt1J0CQQDxorL/rbJ5+35Z 25 | i3IeMcGbEYhtiE+eaLeFwsWILReWRpgiiH7O1TIBxUyBT12nwn3U5HAxUwGHJp+3 26 | dvACkQXPAkEAnapLmMLNtk/iQZwltzLq1fthH+5YyR2H1rd3xzc9gF2knOtnTggB 27 | BlTk31WVkORf1MQ6yC+5KYFXGUuuAOASbwJAK2dKN9r/gCHIpFUD/qB5Yl1X4DTn 28 | +FBfBsvhp4BSCFBN64YRIR3yiZbjEycqb4PkDmWqMXHziE9LySy4F/3syQJADo4E 29 | AIwrNWNWfbwOd0UKDMryAmKca6SAP8AcHJXq5Yi/g4TvunJetdjsb/mUnxWWCyw6 30 | SPSu4TgBdGJaI9aLnQJBAJfVYztNcFFhlFexhEbAb27VnP17v7GOEcArjHhEUipD 31 | zxTLBbyjGnIeU+NH2IbnbRcJuq9CMrUcLkhknyyDDV4= 32 | -----END RSA PRIVATE KEY-----""" 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /mixin_msg_test.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, request 2 | import requests 3 | import json 4 | 5 | import mixin_config 6 | from mixin_api import MIXIN_API 7 | 8 | mixin_api = MIXIN_API(mixin_config) 9 | 10 | 11 | # 启动 Flask 12 | app = Flask(__name__) 13 | 14 | 15 | @app.route('/') 16 | def index(): 17 | # 1. 获得用户的授权 Request Authorization Code 18 | 19 | scope = 'PROFILE:READ+PHONE:READ+CONTACTS:READ+ASSETS:READ' 20 | 21 | get_auth_code_url = 'https://mixin.one/oauth/authorize?client_id='+ mixin_config.client_id+'&scope='+ scope +'&response_type=code' 22 | return redirect(get_auth_code_url) 23 | 24 | 25 | @app.route('/user') 26 | def user(): 27 | 28 | # 2. 取得 Authorization Token 29 | auth_token = get_auth_token() 30 | 31 | 32 | data = mixin_api.getMyProfile(auth_token) 33 | 34 | data_friends = mixin_api.getMyFriends(auth_token) 35 | 36 | data_asset = mixin_api.getMyAssets(auth_token) 37 | 38 | 39 | return '

mixin api

user id:'+data['user_id'] + ' asset friend' 40 | 41 | # 取得 Authorization Token 42 | def get_auth_token(): 43 | get_auth_token_url = 'https://api.mixin.one/oauth/token' 44 | 45 | # 从 url 中取到 code 46 | auth_code = request.args.get('code') 47 | 48 | post_data = { 49 | "client_id": mixin_config.client_id, 50 | "code": auth_code, 51 | "client_secret": mixin_config.client_secret, 52 | } 53 | 54 | r = requests.post(get_auth_token_url, json=post_data) 55 | r_json = r.json() 56 | print(r_json) 57 | 58 | auth_token = r_json['data']['access_token'] 59 | 60 | return auth_token 61 | 62 | if __name__ == '__main__': 63 | app.run(debug=True, host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /mixin_ws_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mixin Python3 Websocket SDK 3 | base on https://github.com/myrual/mixin_client_demo/blob/master/home_of_cnb_robot.py 4 | code by Lee.c 5 | update at 2018.12.2 6 | """ 7 | import json 8 | import uuid 9 | import gzip 10 | import time 11 | from io import BytesIO 12 | import base64 13 | import websocket 14 | import mixin_config 15 | from mixin_api import MIXIN_API 16 | 17 | try: 18 | import thread 19 | except ImportError: 20 | import _thread as thread 21 | 22 | 23 | class MIXIN_WS_API: 24 | 25 | def __init__(self, on_message, on_open=None, on_error=None, on_close=None, on_data=None): 26 | 27 | mixin_api = MIXIN_API(mixin_config) 28 | encoded = mixin_api.genGETJwtToken('/', "", str(uuid.uuid4())) 29 | 30 | if on_open is None: 31 | on_open = MIXIN_WS_API.__on_open 32 | 33 | if on_close is None: 34 | on_close = MIXIN_WS_API.__on_close 35 | 36 | if on_error is None: 37 | on_error = MIXIN_WS_API.__on_error 38 | 39 | if on_data is None: 40 | on_data = MIXIN_WS_API.__on_data 41 | 42 | self.ws = websocket.WebSocketApp("wss://blaze.mixin.one/", 43 | on_message=on_message, 44 | on_error=on_error, 45 | on_close=on_close, 46 | header=["Authorization:Bearer " + encoded.decode()], 47 | subprotocols=["Mixin-Blaze-1"], 48 | on_data=on_data) 49 | 50 | self.ws.on_open = on_open 51 | 52 | """ 53 | run websocket server forever 54 | """ 55 | def run(self): 56 | 57 | while True: 58 | self.ws.run_forever() 59 | 60 | """ 61 | ======================== 62 | WEBSOCKET DEFAULT METHOD 63 | ======================== 64 | """ 65 | 66 | """ 67 | on_open default 68 | """ 69 | @staticmethod 70 | def __on_open(ws): 71 | 72 | def run(*args): 73 | print("ws open") 74 | Message = {"id": str(uuid.uuid1()), "action": "LIST_PENDING_MESSAGES"} 75 | Message_instring = json.dumps(Message) 76 | 77 | fgz = BytesIO() 78 | gzip_obj = gzip.GzipFile(mode='wb', fileobj=fgz) 79 | gzip_obj.write(Message_instring.encode()) 80 | gzip_obj.close() 81 | ws.send(fgz.getvalue(), opcode=websocket.ABNF.OPCODE_BINARY) 82 | while True: 83 | time.sleep(1) 84 | 85 | thread.start_new_thread(run, ()) 86 | 87 | """ 88 | on_data default 89 | """ 90 | @staticmethod 91 | def __on_data(ws, readableString, dataType, continueFlag): 92 | return 93 | 94 | """ 95 | on_close default 96 | """ 97 | 98 | @staticmethod 99 | def __on_close(ws): 100 | return 101 | 102 | """ 103 | on_error default 104 | """ 105 | 106 | @staticmethod 107 | def __on_error(error): 108 | print(error) 109 | 110 | 111 | """ 112 | ================= 113 | REPLY USER METHOD 114 | ================= 115 | """ 116 | 117 | """ 118 | generate a standard message base on Mixin Messenger format 119 | """ 120 | 121 | @staticmethod 122 | def writeMessage(websocketInstance, action, params): 123 | 124 | message = {"id": str(uuid.uuid1()), "action": action, "params": params} 125 | message_instring = json.dumps(message) 126 | 127 | fgz = BytesIO() 128 | gzip_obj = gzip.GzipFile(mode='wb', fileobj=fgz) 129 | gzip_obj.write(message_instring.encode()) 130 | gzip_obj.close() 131 | websocketInstance.send(fgz.getvalue(), opcode=websocket.ABNF.OPCODE_BINARY) 132 | 133 | """ 134 | when receive a message, must reply to server 135 | ACKNOWLEDGE_MESSAGE_RECEIPT ack server received message 136 | """ 137 | @staticmethod 138 | def replayMessage(websocketInstance, msgid): 139 | parameter4IncomingMsg = {"message_id": msgid, "status": "READ"} 140 | Message = {"id": str(uuid.uuid1()), "action": "ACKNOWLEDGE_MESSAGE_RECEIPT", "params": parameter4IncomingMsg} 141 | Message_instring = json.dumps(Message) 142 | fgz = BytesIO() 143 | gzip_obj = gzip.GzipFile(mode='wb', fileobj=fgz) 144 | gzip_obj.write(Message_instring.encode()) 145 | gzip_obj.close() 146 | websocketInstance.send(fgz.getvalue(), opcode=websocket.ABNF.OPCODE_BINARY) 147 | return 148 | 149 | """ 150 | reply a button to user 151 | """ 152 | @staticmethod 153 | def sendUserAppButton(websocketInstance, in_conversation_id, to_user_id, realLink, text4Link, colorOfLink="#0084ff"): 154 | 155 | btn = '[{"label":"' + text4Link + '","action":"' + realLink + '","color":"' + colorOfLink + '"}]' 156 | 157 | btn = base64.b64encode(btn.encode('utf-8')).decode(encoding='utf-8') 158 | 159 | params = {"conversation_id": in_conversation_id, "recipient_id": to_user_id, "message_id": str(uuid.uuid4()), 160 | "category": "APP_BUTTON_GROUP", "data": btn} 161 | return MIXIN_WS_API.writeMessage(websocketInstance, "CREATE_MESSAGE", params) 162 | 163 | """ 164 | reply a contact card to user 165 | """ 166 | 167 | @staticmethod 168 | def sendUserContactCard(websocketInstance, in_conversation_id, to_user_id, to_share_userid): 169 | 170 | btnJson = json.dumps({"user_id": to_share_userid}) 171 | btnJson = base64.b64encode(btnJson.encode('utf-8')).decode('utf-8') 172 | params = {"conversation_id": in_conversation_id, "recipient_id": to_user_id, "message_id": str(uuid.uuid4()), 173 | "category": "PLAIN_CONTACT", "data": btnJson} 174 | return MIXIN_WS_API.writeMessage(websocketInstance, "CREATE_MESSAGE", params) 175 | 176 | """ 177 | reply a text to user 178 | """ 179 | @staticmethod 180 | def sendUserText(websocketInstance, in_conversation_id, to_user_id, textContent): 181 | 182 | textContent = textContent.encode('utf-8') 183 | textContent = base64.b64encode(textContent).decode(encoding='utf-8') 184 | 185 | params = {"conversation_id": in_conversation_id, "recipient_id": to_user_id, "status": "SENT", 186 | "message_id": str(uuid.uuid4()), "category": "PLAIN_TEXT", 187 | "data": textContent} 188 | return MIXIN_WS_API.writeMessage(websocketInstance, "CREATE_MESSAGE", params) 189 | 190 | """ 191 | send user a pay button 192 | """ 193 | @staticmethod 194 | def sendUserPayAppButton(webSocketInstance, in_conversation_id, to_user_id, inAssetName, inAssetID, inPayAmount, linkColor="#0CAAF5"): 195 | payLink = "https://mixin.one/pay?recipient=" + mixin_config.client_id + "&asset=" + inAssetID + "&amount=" + str( 196 | inPayAmount) + '&trace=' + str(uuid.uuid1()) + '&memo=PRS2CNB' 197 | btn = '[{"label":"' + inAssetName + '","action":"' + payLink + '","color":"' + linkColor + '"}]' 198 | 199 | btn = base64.b64encode(btn.encode('utf-8')).decode(encoding='utf-8') 200 | 201 | gameEntranceParams = {"conversation_id": in_conversation_id, "recipient_id": to_user_id, 202 | "message_id": str(uuid.uuid4()), "category": "APP_BUTTON_GROUP", "data": btn} 203 | MIXIN_WS_API.writeMessage(webSocketInstance, "CREATE_MESSAGE", gameEntranceParams) 204 | 205 | @staticmethod 206 | def sendAppCard(websocketInstance, in_conversation_id, to_user_id, asset_id, amount, icon_url, title, description, color="#0080FF", memo=""): 207 | payLink = "https://mixin.one/pay?recipient=" + to_user_id + "&asset=" + asset_id + "&amount=" + \ 208 | amount + "&trace=" + str(uuid.uuid4()) + "&memo=" 209 | card = '{"icon_url":"' + icon_url + '","title":"' + title + \ 210 | '","description":"' + description + '","action":"'+ payLink + '"}' 211 | enCard = base64.b64encode(card.encode('utf-8')).decode(encoding='utf-8') 212 | params = {"conversation_id": in_conversation_id, "message_id": str(uuid.uuid4()), 213 | "category": "APP_CARD", "status": "SENT", "data": enCard} 214 | return MIXIN_WS_API.writeMessage(websocketInstance, "CREATE_MESSAGE", params) 215 | 216 | @staticmethod 217 | def sendAppButtonGroup(websocketInstance, in_conversation_id, to_user_id, buttons): 218 | buttonsStr = '[' + ','.join(str(btn) for btn in buttons) +']' 219 | enButtons = base64.b64encode(buttonsStr.encode('utf-8')).decode(encoding='utf-8') 220 | params = {"conversation_id": in_conversation_id, "recipient_id": to_user_id, 221 | "message_id": str(uuid.uuid4()), 222 | "category": "APP_BUTTON_GROUP", "status": "SENT", "data": enButtons} 223 | return MIXIN_WS_API.writeMessage(websocketInstance, "CREATE_MESSAGE", params) 224 | 225 | @staticmethod 226 | def packButton(to_user_id, asset_id, amount, label, color="#FF8000", memo=""): 227 | payLink = "https://mixin.one/pay?recipient=" + to_user_id + "&asset=" + asset_id + "&amount=" + \ 228 | amount + "&trace=" + str(uuid.uuid4()) + "&memo=" 229 | button = '{"label":"' + label + '","color":"' + color + '","action":"' + payLink + '"}' 230 | return button 231 | 232 | 233 | -------------------------------------------------------------------------------- /test_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Mixin API TEST for Python 3.x 4 | env: python 3.x 5 | code by lee.c 6 | update at 2018.12.2 7 | """ 8 | from mixin_api import MIXIN_API 9 | import mixin_config 10 | import time 11 | 12 | mixin_api = MIXIN_API(mixin_config) 13 | 14 | 15 | 16 | 17 | transfer2user_id = 'd33f7efd-4b0b-41ff-baa3-b22ea40eb44f' # my user id 18 | 19 | # cuiniubi token asset id 20 | CNB_ASSET_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c" 21 | 22 | # test robot transfer to user_id 23 | # for i in range(1, 5): 24 | # r = mixin_api.transferTo(transfer2user_id, CNB_ASSET_ID, i, "转账次数:" + str(i)) 25 | # time.sleep(1) 26 | 27 | mixin_api.getTransfer('bc52ff5a-f610-11e8-8e2a-28c63ffad907') 28 | 29 | # mixin_api.getTransfer('13f4c4de-f572-11e8-94cc-00e04c6aa167') 30 | # 31 | # 32 | # mixin_api_robot.getAsset(CNB_ASSET_ID) 33 | # 34 | # 35 | # mixin_api.topAssets() 36 | # 37 | # print('snapshot') 38 | # mixin_api.snapshot('3565a804-9932-4c3c-8280-b0222166eec7') 39 | 40 | # 289d6876-79ff-4699-9901-7a670953eef8 41 | mixin_api.snapshot('289d6876-79ff-4699-9901-7a670953eef8') 42 | 43 | -------------------------------------------------------------------------------- /ws_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Mixin Messenger Robot Websocket methods 3 | code by Lee.c 4 | update at 2018.12.2 5 | """ 6 | 7 | from mixin_ws_api import MIXIN_WS_API 8 | from mixin_api import MIXIN_API 9 | import mixin_config 10 | 11 | import json 12 | import time 13 | from io import BytesIO 14 | import base64 15 | import gzip 16 | 17 | try: 18 | import thread 19 | except ImportError: 20 | import _thread as thread 21 | 22 | 23 | def on_message(ws, message): 24 | 25 | inbuffer = BytesIO(message) 26 | 27 | f = gzip.GzipFile(mode="rb", fileobj=inbuffer) 28 | rdata_injson = f.read() 29 | rdata_obj = json.loads(rdata_injson) 30 | action = rdata_obj["action"] 31 | 32 | if action not in ["ACKNOWLEDGE_MESSAGE_RECEIPT", "CREATE_MESSAGE", "LIST_PENDING_MESSAGES"]: 33 | print("unknow action") 34 | return 35 | 36 | if action == "ACKNOWLEDGE_MESSAGE_RECEIPT": 37 | return 38 | 39 | 40 | if action == "CREATE_MESSAGE": 41 | 42 | data = rdata_obj["data"] 43 | msgid = data["message_id"] 44 | typeindata = data["type"] 45 | categoryindata = data["category"] 46 | userId = data["user_id"] 47 | conversationId = data["conversation_id"] 48 | dataindata = data["data"] 49 | created_at = data["created_at"] 50 | updated_at = data["updated_at"] 51 | 52 | realData = base64.b64decode(dataindata) 53 | 54 | MIXIN_WS_API.replayMessage(ws, msgid) 55 | 56 | print('userId', userId) 57 | print("created_at",created_at) 58 | 59 | 60 | if 'error' in rdata_obj: 61 | return 62 | 63 | if categoryindata not in ["SYSTEM_ACCOUNT_SNAPSHOT", "PLAIN_TEXT", "SYSTEM_CONVERSATION", "PLAIN_STICKER", "PLAIN_IMAGE", "PLAIN_CONTACT"]: 64 | print("unknow category") 65 | return 66 | 67 | if categoryindata == "PLAIN_TEXT" and typeindata == "message": 68 | realData = realData.lower().decode('utf-8') 69 | 70 | 71 | if 'hi' == realData: 72 | introductionContent = 'welcome to MyFirstRobot\n[hihi] reply n times text\n[c] send a contact card\n[b] send a link button\n[p] you need to pay\n[t] transfer to you' 73 | MIXIN_WS_API.sendUserText(ws, conversationId, userId, introductionContent) 74 | return 75 | 76 | if 'hihi' == realData: 77 | introductionContent = '你好呀 ' 78 | for i in range(3): 79 | MIXIN_WS_API.sendUserText(ws, conversationId, userId, introductionContent + str(i)) 80 | time.sleep(1) 81 | return 82 | 83 | if 'c' == realData: 84 | print('send a contact card') 85 | MIXIN_WS_API.sendUserContactCard(ws, conversationId, userId, "d33f7efd-4b0b-41ff-baa3-b22ea40eb44f") 86 | return 87 | 88 | if 'b' == realData: 89 | print('send a link button') 90 | MIXIN_WS_API.sendUserAppButton(ws, conversationId, userId, "https://github.com/includeleec/mixin-python3-sdk", "点我了解 Mixin Python3 SDK") 91 | return 92 | 93 | if 'p' == realData: 94 | print('you need to pay') 95 | CNB_ASSET_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c" 96 | MIXIN_WS_API.sendUserPayAppButton(ws, conversationId, userId, "给点钱吧", CNB_ASSET_ID, 1, "#ff0033") 97 | return 98 | 99 | if 't' == realData: 100 | print('transfer to you') 101 | CNB_ASSET_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c" 102 | mixin_api.transferTo(userId, CNB_ASSET_ID, 2, "滴水之恩") 103 | return 104 | 105 | elif categoryindata == "PLAIN_TEXT": 106 | print("PLAIN_TEXT but unkonw:") 107 | 108 | 109 | 110 | if __name__ == "__main__": 111 | 112 | mixin_api = MIXIN_API(mixin_config) 113 | 114 | mixin_ws = MIXIN_WS_API(on_message=on_message) 115 | 116 | mixin_ws.run() 117 | 118 | --------------------------------------------------------------------------------