Skip to content

Commit c7093f4

Browse files
authored
Merge pull request #34 from pedohorse/dev
Dev
2 parents 569e0c9 + f921d90 commit c7093f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+5800
-69
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ simple plain text snippet exchange for Houdini
33

44
for simple and fast exchange of node packs through any messenger
55

6-
**Hpaste** works/tested for **Houdini 19.0, 18.5, 18.0, 17.5, 17.0, 16.5, 16.0, 15.5**. Should work also for 15.0 and maybe even less, to the point when Qt appeared in Houdini
7-
**HCollections** should work in **Houdini 19.0 18.x, 17.x, 16.x, 15.5** with both Qt4 and Qt5
6+
**Hpaste** works/tested for **Houdini 19.5, 19.0, 18.5, 18.0, 17.5, 17.0, 16.5, 16.0, 15.5**. Should work also for 15.0 and maybe even less, to the point when Qt appeared in Houdini
7+
**HCollections** should work in **Houdini 19.5, 19.0, 18.5, 18.0, 17.x, 16.x, 15.5** with both Qt4 and Qt5
88
Though new features like **Inspector** are made without Qt4 backwards compatibility, therefore they won't work in qt4 versions of houdini 17 and older
99

10-
Works for both python2 and python3 builds. however there are some early 18.5 and 18.0 python3 builds that lack certain libs, but that bug has been fixed in later builds.
10+
Works for both python2 and python3 builds. however there are some early 18.5 and 18.0 python3 builds that lack certain libs, but that bug has been fixed in later builds.
1111

12-
**Note: 18.0.348 production build is known to have Qt issues, which seems to be solved starting from build 353**
12+
**Note: 19.5.303 production build has a bug in it's version of PyCrypto, so cryptography does not work. Hope SideFX fixes it in the next build.**
13+
**Note: 18.0.348 production build is known to have Qt issues, which seems to be solved starting from build 353**
1314

1415
You can read a bit more about it in here:
1516
* https://cgallin.blogspot.com/2017/09/hpaste.html

python2.7libs/hpaste/hpaste.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,19 @@ def nodesToString(nodes, transfer_assets=None, encryption_type=None, clean_metad
200200
newcode = re.sub(r'# Code to establish connections for.+\n.+\n', '# filtered lines\n', newcode, 1)
201201
code += newcode
202202
elif algtype == 1 or algtype == 2:
203-
if transfer_assets: # added in version 2.1
203+
if transfer_assets: # added in version 2.1
204204
# scan for nonstandard asset definitions
205205
hfs = os.environ['HFS']
206+
already_stashed = set()
206207
for elem in nodes:
207-
if not isinstance(elem, hou.Node): continue
208+
if not isinstance(elem, hou.Node):
209+
continue
208210
for node in [elem] + list(elem.allSubChildren()):
211+
if not isinstance(node, hou.Node):
212+
continue
213+
if node.type().nameComponents() in already_stashed:
214+
continue
215+
already_stashed.add(node.type().nameComponents())
209216
definition = node.type().definition()
210217
if definition is None:
211218
continue

python2.7libs/optionsDialogTester.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

python3.7libs/hpaste/hpaste.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@
1313
import base64
1414
import bz2
1515

16-
from Crypto.Cipher import AES
17-
from Crypto import Random as CRandom
16+
crypto_available = True
17+
try:
18+
from Crypto.Cipher import AES
19+
from Crypto import Random as CRandom
20+
except Exception: # not just import error, in case of buggy h19.5p3.9 it's syntax error
21+
crypto_available = False
22+
AES = Random = CRandom = None
23+
1824
import struct
1925

2026
import tempfile
@@ -102,8 +108,10 @@ def getChildContext(node, houver):
102108

103109

104110
def getSerializer(enctype: Optional[str] = None, **kwargs) -> Tuple[Optional[dict], Callable[[bytes], bytes]]: # TODO: makes more sense for serializer to return str
105-
rng = CRandom.new()
106111
if enctype == 'AES':
112+
if not crypto_available:
113+
raise RuntimeError('PyCrypto is not available, cannot encrypt/decrypt')
114+
rng = CRandom.new()
107115
key = kwargs['key']
108116
mode = kwargs.get('mode', AES.MODE_CBC)
109117
iv = kwargs.get('iv', rng.read(AES.block_size))
@@ -126,6 +134,8 @@ def _ser(x: bytes):
126134

127135
def getDeserializer(enctype: Optional[str] = None, **kwargs) -> Callable[[bytes], bytes]:
128136
if enctype == 'AES':
137+
if not crypto_available:
138+
raise RuntimeError('PyCrypto is not available, cannot encrypt/decrypt')
129139
key = kwargs['key']
130140
if key is None:
131141
raise NoKeyError('no decryption key provided for encryption type AES')
@@ -208,9 +218,16 @@ def nodesToString(nodes, transfer_assets=None, encryption_type=None, clean_metad
208218
if transfer_assets: # added in version 2.1
209219
# scan for nonstandard asset definitions
210220
hfs = os.environ['HFS']
221+
already_stashed = set()
211222
for elem in nodes:
212-
if not isinstance(elem, hou.Node): continue
223+
if not isinstance(elem, hou.Node):
224+
continue
213225
for node in [elem] + list(elem.allSubChildren()):
226+
if not isinstance(node, hou.Node):
227+
continue
228+
if node.type().nameComponents() in already_stashed:
229+
continue
230+
already_stashed.add(node.type().nameComponents())
214231
definition = node.type().definition()
215232
if definition is None:
216233
continue

python3.7libs/hpaste/hpasteshelffunctions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import hou
22
import random
33
import string
4-
from Crypto.Cipher import AES # just for blocksize and constants
4+
crypto_available = True
5+
try:
6+
from Crypto.Cipher import AES # just for blocksize and constants
7+
except Exception: # not just import error, in case of buggy h19.5p3.9 it's syntax error
8+
crypto_available = False
9+
AES = None
510

611
from PySide2.QtWidgets import QApplication
712
from PySide2 import QtCore as qtc
@@ -35,6 +40,9 @@ def hcopyweb():
3540
key = None
3641
encparms = {}
3742
if enctype == 'AES-CBC':
43+
if not crypto_available:
44+
hou.ui.displayMessage("PyCrypto seem to be broken in your version of houdini, cannot encrypt/decrypt")
45+
return
3846
key = ''.join([random.choice(string.ascii_letters + string.digits) for x in range(AES.block_size)])
3947
encparms = {'mode': AES.MODE_CBC}
4048
enctype = 'AES'

python3.7libs/optionsDialogTester.py

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import re
2+
import json
3+
from urllib import request
4+
from .nethelper import urlopen_nt
5+
try:
6+
from PySide2.QtWidgets import QDialog, QVBoxLayout, QLabel, QSizePolicy, QPushButton, QMessageBox
7+
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage
8+
from PySide2.QtCore import Slot, Qt, QUrl
9+
except ImportError:
10+
raise NotImplementedError('web auth implemented only for QT5. Sorry, people who still use houdini 16.5. You will have to create access token manually. contact me to ask how.')
11+
12+
import time
13+
14+
15+
class QGithubDeviceAuthDialog(QDialog):
16+
def __init__(self, client_id='42e8e8e9d844e45c2d05', hint_username=None, parent=None):
17+
super(QGithubDeviceAuthDialog, self).__init__(parent=parent)
18+
self.setWindowTitle('Log into your GitHub account and enter this code')
19+
20+
self.__webview = QWebEngineView(parent=self)
21+
self.__webview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
22+
self.__webview.urlChanged.connect(self.on_url_changed)
23+
24+
self.__infolaabel = QLabel('<p style="font-size:14px">'
25+
'<br>You need to allow hpaste to modify your gists on github. for that you need to log in to your account and authorize hpaste.\n'
26+
'<br>You can do it in <b>any</b> browser, not only in this window. Just go to <a href="https://github.com/login/device">https://github.com/login/device</a> and enter the code below.\n'
27+
'<br>close this window when you are done'
28+
'</p>', parent=self)
29+
self.__infolaabel.setTextFormat(Qt.RichText)
30+
self.__infolaabel.setTextInteractionFlags(Qt.TextBrowserInteraction)
31+
self.__infolaabel.setOpenExternalLinks(True)
32+
self.__devidlabel = QLabel(parent=self)
33+
self.__devidlabel.setTextInteractionFlags(Qt.TextBrowserInteraction)
34+
ff = self.__devidlabel.font()
35+
ff.setPointSize(64)
36+
self.__devidlabel.setFont(ff)
37+
38+
self.__reload_button = QPushButton('log in to a different account', parent=self)
39+
self.__reload_button.clicked.connect(self.reload_button_clicked)
40+
41+
self.__layout = QVBoxLayout()
42+
self.setLayout(self.__layout)
43+
self.__layout.addWidget(self.__infolaabel)
44+
self.__layout.addWidget(self.__devidlabel)
45+
self.__layout.addWidget(self.__reload_button)
46+
self.__layout.addWidget(self.__webview)
47+
self.__result = None
48+
49+
# self.setGeometry(512, 256, 1024, 768)
50+
self.resize(1024, 860)
51+
52+
# init auth process
53+
self.__client_id = client_id
54+
self.__headers = {'User-Agent': 'HPaste', 'Accept': 'application/json', 'charset': 'utf-8', 'Content-Type': 'application/json'}
55+
self.__hint_username = hint_username
56+
57+
self.__webprofile = None
58+
self.__webpage = None
59+
60+
self.__device_code = None
61+
self.__interval = None
62+
self.__await_login_redirect = False
63+
self.reinit_code()
64+
65+
def reinit_code(self):
66+
reqdata = {'client_id': self.__client_id,
67+
'scope': 'gist'}
68+
req = request.Request('https://github.com/login/device/code', data=json.dumps(reqdata).encode('UTF-8'), headers=self.__headers)
69+
req.get_method = lambda: 'POST'
70+
code, ret = urlopen_nt(req)
71+
if code != 200:
72+
raise RuntimeError('code %d when trying to register device' % code)
73+
init_data = json.loads(ret.read().decode('UTF-8'))
74+
print(init_data)
75+
self.__device_code = init_data['device_code']
76+
self.__interval = init_data.get('interval', 5)
77+
url = init_data['verification_uri']
78+
79+
self.__await_login_redirect = True
80+
self.__webprofile = QWebEngineProfile(parent=self.__webview)
81+
self.__webpage = QWebEnginePage(self.__webprofile, parent=self.__webview) # just to be sure they are deleted in proper order
82+
self.__webview.setPage(self.__webpage)
83+
self.__webview.load(QUrl(url))
84+
self.__devidlabel.setText('code: %s' % (init_data['user_code'],))
85+
86+
def get_result(self):
87+
return self.__result
88+
89+
def closeEvent(self, event):
90+
# here we assume user has done his part, so lets get checking
91+
for attempt in range(5):
92+
reqdata = {'client_id': self.__client_id,
93+
'device_code': self.__device_code,
94+
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'}
95+
req = request.Request('https://github.com/login/oauth/access_token', data=json.dumps(reqdata).encode('UTF-8'), headers=self.__headers)
96+
req.get_method = lambda: 'POST'
97+
code, ret = urlopen_nt(req)
98+
if code == 200:
99+
rep = json.loads(ret.read().decode('UTF-8'))
100+
print(rep)
101+
if 'error' not in rep: # a SUCC
102+
headers = {'User-Agent': 'HPaste',
103+
'Authorization': 'Token %s' % rep['access_token'],
104+
'Accept': 'application/vnd.github.v3+json'}
105+
req = request.Request(r'https://api.github.com/user', headers=headers)
106+
usercode, userrep = urlopen_nt(req)
107+
if usercode != 200:
108+
raise RuntimeError('could not probe! %d' % (usercode,))
109+
userdata = json.loads(userrep.read().decode('UTF-8'))
110+
print(userdata)
111+
112+
self.__result = {'token': rep['access_token'],
113+
'user': userdata['login']}
114+
break
115+
116+
# NO SUCC
117+
errcode = rep['error']
118+
if errcode == 'authorization_pending':
119+
# note that this error will happen if user just closes down the window
120+
break
121+
elif errcode == 'slow_down':
122+
self.__interval = rep.get('interval', self.__interval + 5)
123+
time.sleep(self.__interval)
124+
continue
125+
elif errcode == 'expired_token':
126+
if QMessageBox.warning(self, 'device code expired', 'it took you too long to enter the code, now it\'s expired.\nWant to retry?', buttons=QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
127+
event.reject()
128+
self.reinit_code()
129+
return
130+
break
131+
elif errcode == 'unsupported_grant_type':
132+
raise RuntimeError('unsupported grant type. probably github changed API. need to update the plugin')
133+
elif errcode == 'incorrect_client_credentials':
134+
raise RuntimeError('incorect client id. probably pedohorse changed hpaste id for some reason. update the plugin')
135+
elif errcode == 'incorrect_device_code':
136+
if QMessageBox.warning(self, 'bad device code', 'server reported wrong device code\nWant to retry?', buttons=QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
137+
event.reject()
138+
self.reinit_code()
139+
return
140+
break
141+
elif errcode == 'access_denied':
142+
# means user denied the request
143+
break
144+
else:
145+
raise RuntimeError('unexpected error: %s' % (json.dumps(rep),))
146+
else:
147+
raise RuntimeError('bad return code. server reported with bad return code %d' % code)
148+
else:
149+
if QMessageBox.warning(self, 'unknown error', 'could not manage to check authorization in reasonable amount of attempts\nWant to retry?', buttons=QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
150+
event.reject()
151+
self.reinit_code()
152+
return
153+
154+
return super(QGithubDeviceAuthDialog, self).closeEvent(event)
155+
156+
@Slot(object)
157+
def on_url_changed(self, qurl):
158+
url = qurl.toString()
159+
print(url)
160+
if not self.__await_login_redirect:
161+
return
162+
if qurl.path() != '/login':
163+
return
164+
self.__await_login_redirect = False
165+
if self.__hint_username is not None:
166+
if qurl.hasQuery():
167+
qurl.setQuery('&'.join((qurl.query(), 'login=%s' % self.__hint_username)))
168+
else:
169+
qurl.setQuery('login=%s' % self.__hint_username)
170+
print('redir to %s' % qurl.toString)
171+
self.__webview.load(qurl)
172+
173+
@Slot()
174+
def reload_button_clicked(self):
175+
self.reinit_code()
176+
177+
178+
if __name__ == '__main__': # testing
179+
import sys
180+
import string
181+
import random
182+
from PySide2.QtWidgets import QApplication
183+
qapp = QApplication(sys.argv)
184+
# w = QWebAuthDialog('https://www.google.com', r'https://www.google.com/search\?(.*)')
185+
webauthstate = ''.join(random.choice(string.ascii_letters) for _ in range(32))
186+
webauthparms = {'client_id': '42e8e8e9d844e45c2d05',
187+
'redirect_uri': 'https://github.com/login/oauth/success',
188+
'scope': 'gist',
189+
'state': webauthstate}
190+
w = QGithubDeviceAuthDialog(client_id='42e8e8e9d844e45c2d05', hint_username='ololovich', parent=None)
191+
res = w.exec_()
192+
print(res == QGithubDeviceAuthDialog.Accepted)
193+
print(w.get_result())
194+

0 commit comments

Comments
 (0)