├── .gitignore
├── mixin_config.default.py
├── README.md
├── test_api.py
├── mixin_config.py
├── mixin_msg_test.py
├── ws_test.py
├── mixin_ws_api.py
└── mixin_api.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------