Skip to content
This repository was archived by the owner on Dec 20, 2021. It is now read-only.

Commit 430d8ac

Browse files
authored
Merge pull request #13 from Katistic/master
Two-Factor-Auth login + examples and small fixes
2 parents 525e388 + 74295f6 commit 430d8ac

File tree

8 files changed

+218
-34
lines changed

8 files changed

+218
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,4 @@ dmypy.json
128128
publish.cmd
129129
test.py
130130
atest.py
131+
apitester.py

examples/async/Login Logout.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import asyncio
2+
import vrcpy
3+
4+
# Normal vanilla login
5+
async def main():
6+
# Initialise vrcpy wrapper client and login with username + password
7+
client = vrcpy.AClient()
8+
await client.login("***REMOVED***", "***REMOVED***")
9+
10+
# Close client session, invalidate auth cookie
11+
await client.logout()
12+
13+
# 2-Factor-Auth account login
14+
def twofactorauth():
15+
# Initialise vrcpy wrapper client
16+
client = vrcpy.AClient()
17+
18+
# Login with username + password, then verify 2-factor-auth code
19+
# Can be done in 1 line via:
20+
# await client.login2fa("username", "password", code="123456", verify=True)
21+
await client.login2fa("username", "password")
22+
await client.verify2fa("123456")
23+
24+
# Close client session, invalidate auth cookie
25+
await client.logout()
26+
27+
def run_main():
28+
asyncio.get_event_loop().run_until_complete(main())
29+
30+
def run_2fa():
31+
asyncio.get_event_loop().run_until_complete(twofactorauth())

examples/sync/Login Logout.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import vrcpy
2+
3+
# Normal vanilla login
4+
def main():
5+
# Initialise vrcpy wrapper client and login with username + password
6+
client = vrcpy.Client()
7+
client.login("username", "password")
8+
9+
# Close client session, invalidate auth cookie
10+
client.logout()
11+
12+
# 2-Factor-Auth account login
13+
def twofactorauth():
14+
# Initialise vrcpy wrapper client
15+
client = vrcpy.Client()
16+
17+
# Login with username + password, then verify 2-factor-auth code
18+
# Can be done in 1 line via:
19+
# client.login2fa("username", "password", code="123456", verify=True)
20+
client.login2fa("username", "password")
21+
client.verify2fa("123456")
22+
23+
# Close client session, invalidate auth cookie
24+
client.logout()

vrcpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__title__ = "vrcpy"
22
__author__ = "Katistic"
3-
__version__ = "0.6.5"
3+
__version__ = "0.6.7"
44

55
from vrcpy.client import Client
66
from vrcpy.client import AClient

vrcpy/client.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,69 @@ def login(self, username, password):
274274
self.me = objects.CurrentUser(self, resp["data"])
275275
self.loggedIn = True
276276

277+
def login2fa(self, username, password, code=None, verify=False):
278+
'''
279+
Used to initialize client for use (for accounts with 2FactorAuth)
280+
281+
username, string
282+
Username of VRC account
283+
284+
password, string
285+
Password of VRC account
286+
287+
code, string
288+
2FactorAuth code
289+
290+
verify, boolean
291+
Whether to verify 2FactorAuth code, or leave for later
292+
293+
This will ignore the RequiresTwoFactorAuthError exception, so be careful!
294+
If kwarg verify is False, Client.verify2fa() must be called after
295+
'''
296+
297+
if self.loggedIn: raise AlreadyLoggedInError("Client is already logged in")
298+
299+
auth = username+":"+password
300+
auth = str(base64.b64encode(auth.encode()))[2:-1]
301+
302+
resp = None
303+
304+
try:
305+
resp = self.api.call("/auth/user", headers={"Authorization": "Basic "+auth}, no_auth=True, verify=False)
306+
raise_for_status(resp)
307+
except RequiresTwoFactorAuthError:
308+
self.api.set_auth(auth)
309+
self.api.session.cookies.set("auth", resp["response"].cookies["auth"]) # Auth cookieeee
310+
if verify:
311+
self.needsVerification = True
312+
self.verify2fa(code)
313+
else: self.needsVerification = True
314+
315+
def verify2fa(self, code):
316+
'''
317+
Used to finish initializing client for use after Client.login2fa()
318+
319+
code, string
320+
2FactorAuth code
321+
'''
322+
323+
if self.loggedIn: raise AlreadyLoggedInError("Client is already logged in")
324+
325+
resp = self.api.call("/auth/twofactorauth/totp/verify", "POST", json={"code": code})
326+
resp = self.api.call("/auth/user")
327+
328+
self.me = objects.CurrentUser(self, resp["data"])
329+
self.loggedIn = True
330+
self.needsVerification = False
331+
277332
def __init__(self, verify=True, caching=True):
278333
self.api = Call(verify)
279334
self.loggedIn = False
280335
self.me = None
281336
self.caching = caching
282337

338+
self.needsVerification = False
339+
283340
class AClient(Client):
284341
'''
285342
Main client interface for VRC
@@ -541,10 +598,70 @@ async def login(self, username, password):
541598

542599
await self.me.cacheTask
543600

601+
async def login2fa(self, username, password, code=None, verify=False):
602+
'''
603+
Used to initialize client for use (for accounts with 2FactorAuth)
604+
605+
username, string
606+
Username of VRC account
607+
608+
password, string
609+
Password of VRC account
610+
611+
code, string
612+
2FactorAuth code
613+
614+
verify, boolean
615+
Whether to verify 2FactorAuth code, or leave for later
616+
617+
This will ignore the RequiresTwoFactorAuthError exception, so be careful!
618+
If kwarg verify is False, AClient.verify2fa() must be called after
619+
'''
620+
621+
if self.loggedIn: raise AlreadyLoggedInError("Client is already logged in")
622+
623+
auth = username+":"+password
624+
auth = str(base64.b64encode(auth.encode()))[2:-1]
625+
626+
resp = None
627+
628+
try:
629+
resp = await self.api.call("/auth/user", headers={"Authorization": "Basic "+auth}, no_auth=True, verify=False)
630+
raise_for_status(resp)
631+
except RequiresTwoFactorAuthError:
632+
self.api.openSession(auth)
633+
self.api.session.cookie_jar.update_cookies([["auth", resp["response"].headers["Set-Cookie"].split(';')[0].split("=")[1]]])
634+
635+
if verify:
636+
self.needsVerification = True
637+
await self.verify2fa(code)
638+
else:
639+
self.needsVerification = True
640+
641+
async def verify2fa(self, code):
642+
'''
643+
Used to finish initializing client for use after AClient.login2fa()
644+
645+
code, string
646+
2FactorAuth code
647+
'''
648+
649+
if self.loggedIn: raise AlreadyLoggedInError("Client is already logged in")
650+
651+
await self.api.call("/auth/twofactorauth/totp/verify", "POST", json={"code": code})
652+
resp = await self.api.call("/auth/user")
653+
654+
self.me = aobjects.CurrentUser(self, resp["data"])
655+
self.loggedIn = True
656+
self.needsVerification = False
657+
658+
await self.me.cacheTask
659+
544660
def __init__(self, verify=True, caching=True):
545661
super().__init__()
546-
547662
self.api = ACall(verify=verify)
548663
self.loggedIn = False
549664
self.me = None
550665
self.caching = caching
666+
667+
self.needsVerification = False

vrcpy/errors.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ class NotAuthenticated(Exception):
1010
## When user tries to call authenticated requests without setting b64_auth
1111
pass
1212

13-
class TwoFactorAuthNotSupportedError(Exception):
14-
## When trying to login with 2fa enabled account
13+
class RequiresTwoFactorAuthError(Exception):
14+
## When trying to login with 2fa enabled account without Client.login2fa()
15+
pass
16+
17+
class InvalidTwoFactorAuth(Exception):
18+
## When 2fa code passed for verification is incorrect
1519
pass
1620

1721
class AlreadyLoggedInError(Exception):

vrcpy/objects.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ def _objectIntegrety(self, obj):
4545
if self.only == []:
4646
for key in self.unique:
4747
if not key in obj:
48-
raise IntegretyError("Object does not have unique key ("+key+") for "+self.objType)
48+
raise IntegretyError("Object does not have unique key ("+key+") for "+self.objType+" (Class definition may be outdated, please make an issue on github)")
4949
else:
5050
for key in obj:
5151
if not key in self.only:
52-
raise IntegretyError("Object has key not found in "+self.objType)
52+
raise IntegretyError("Object has key not found in "+self.objType+" (Class definition may be outdated, please make an issue on github)")
5353
for key in self.only:
5454
if not key in obj:
55-
raise IntegretyError("Object does not have requred key ("+key+") for "+self.objType)
55+
raise IntegretyError("Object does not have requred key ("+key+") for "+self.objType+" (Class definition may be outdated, please make an issue on github)")
5656

5757
## Avatar Objects
5858

vrcpy/request.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ def raise_for_status(resp):
1010
resp["data"] = json.loads(resp["data"].decode())
1111

1212
def handle_400():
13-
if resp["data"]["error"]["message"] == "These users are not friends":
14-
raise NotFriendsError("These users are not friends")
15-
elif resp["data"]["error"]["message"] == "\"Users are already friends!\"":
16-
raise AlreadyFriendsError("Users are already friends!")
13+
if "error" in resp["data"]:
14+
if resp["data"]["error"]["message"] == "These users are not friends":
15+
raise NotFriendsError("These users are not friends")
16+
elif resp["data"]["error"]["message"] == "\"Users are already friends!\"":
17+
raise AlreadyFriendsError("Users are already friends!")
18+
elif "verified" in resp["data"]:
19+
raise InvalidTwoFactorAuth("2FactorAuth code is invalid.")
1720

1821
def handle_401():
19-
raise IncorrectLoginError(resp["data"]["error"]["message"])
22+
if "requiresTwoFactorAuth" in resp["data"]["error"]["message"]\
23+
or "Requires Two-Factor Authentication" in resp["data"]["error"]["message"]:
24+
raise RequiresTwoFactorAuthError("Account is 2FactorAuth protected.")
25+
elif "Invalid Username or Password" in resp["data"]["error"]["message"]:
26+
raise IncorrectLoginError(resp["data"]["error"]["message"])
2027

2128
def handle_404():
2229
msg = ""
@@ -37,7 +44,7 @@ def handle_404():
3744

3845
if resp["status"] in switch: switch[resp["status"]]()
3946
if resp["status"] != 200: raise GeneralError("Unhandled error occured: "+str(resp["data"]))
40-
if "requiresTwoFactorAuth" in resp["data"]: raise TwoFactorAuthNotSupportedError("2FA is not supported yet.")
47+
if "requiresTwoFactorAuth" in resp["data"]: raise RequiresTwoFactorAuthError("Account is 2FactorAuth protected.")
4148

4249
class ACall:
4350
def __init__(self, loop=asyncio.get_event_loop(), verify=True):
@@ -62,9 +69,9 @@ async def closeSession(self):
6269
await self.session.close()
6370
self.session = None
6471

65-
async def call(self, path, method="GET", headers={}, params={}, json={}, no_auth=False):
72+
async def call(self, path, method="GET", headers={}, params={}, json={}, no_auth=False, verify=True):
6673
if no_auth:
67-
return await self._call(path, method, headers, params, json)
74+
return await self._call(path, method, headers, params, json, verify)
6875

6976
if self.apiKey == None:
7077
async with self.session.get("https://api.vrchat.cloud/api/1/config", verify_ssl=self.verify) as resp:
@@ -89,19 +96,19 @@ async def call(self, path, method="GET", headers={}, params={}, json={}, no_auth
8996
try: json = await resp.json()
9097
except: json = None
9198

92-
resp = {"status": resp.status, "data": json if not json == None else content}
93-
raise_for_status(resp)
99+
resp = {"status": resp.status, "response": resp, "data": json if not json == None else content}
100+
if verify: raise_for_status(resp)
94101
return resp
95102

96103
json = await resp.json()
97104
status = resp.status
98105

99-
resp = {"status": status, "data": json}
106+
resp = {"status": status, "response": resp, "data": json}
100107

101-
raise_for_status(resp)
108+
if verify: raise_for_status(resp)
102109
return resp
103110

104-
async def _call(self, path, method="GET", headers={}, params={}, json={}):
111+
async def _call(self, path, method="GET", headers={}, params={}, json={}, verify=True):
105112
h = {
106113
"user-agent": "",
107114
}
@@ -132,14 +139,14 @@ async def _call(self, path, method="GET", headers={}, params={}, json={}):
132139
try: json = await resp.json()
133140
except: json = None
134141

135-
return {"status": resp.status, "data": json if not json == None else content}
142+
return {"status": resp.status, "response": resp, "data": json if not json == None else content}
136143

137144
json = await resp.json()
138145
status = resp.status
139146

140-
resp = {"status": status, "data": json}
147+
resp = {"status": status, "response": resp, "data": json}
141148

142-
raise_for_status(resp)
149+
if verify: raise_for_status(resp)
143150
return resp
144151

145152
class Call:
@@ -157,11 +164,11 @@ def new_session(self):
157164
self.session = requests.Session()
158165
self.b64_auth = None
159166

160-
def call(self, path, method="GET", headers={}, params={}, json={}, no_auth=False):
167+
def call(self, path, method="GET", headers={}, params={}, json={}, no_auth=False, verify=True):
161168
headers["user-agent"] = ""
162169

163170
if no_auth:
164-
return self._call(path, method, headers, params, json)
171+
return self._call(path, method, headers, params, json, verify)
165172

166173
if self.b64_auth == None:
167174
raise NotAuthenticated("Tried to do authenticated request without setting b64 auth (Call.set_auth(b64_auth))!")
@@ -189,17 +196,17 @@ def call(self, path, method="GET", headers={}, params={}, json={}, no_auth=False
189196
try: json = resp.json()
190197
except: json = None
191198

192-
resp = {"status": resp.status_code, "data": json if not json == None else resp.content}
199+
resp = {"status": resp.status_code, "response": resp, "data": json if not json == None else resp.content}
193200

194-
raise_for_status(resp)
201+
if verify: raise_for_status(resp)
195202
return resp
196203

197-
resp = {"status": resp.status_code, "data": resp.json()}
204+
resp = {"status": resp.status_code, "response": resp, "data": resp.json()}
198205

199-
raise_for_status(resp)
206+
if verify: raise_for_status(resp)
200207
return resp
201208

202-
def _call(self, path, method="GET", headers={}, params={}, json={}):
209+
def _call(self, path, method="GET", headers={}, params={}, json={}, verify=True):
203210
if self.apiKey == None:
204211
resp = requests.get("https://api.vrchat.cloud/api/1/config", headers=headers, verify=self.verify)
205212
assert resp.status_code == 200
@@ -222,12 +229,12 @@ def _call(self, path, method="GET", headers={}, params={}, json={}):
222229
try: json = resp.json()
223230
except: json = None
224231

225-
resp = {"status": resp.status_code, "data": json if not json == None else resp.content}
232+
resp = {"status": resp.status_code, "response": resp, "data": json if not json == None else resp.content}
226233

227-
raise_for_status(resp)
234+
if verify: raise_for_status(resp)
228235
return resp
229236

230-
resp = {"status": resp.status_code, "data": resp.json()}
237+
resp = {"status": resp.status_code, "response": resp, "data": resp.json()}
231238

232-
raise_for_status(resp)
239+
if verify: raise_for_status(resp)
233240
return resp

0 commit comments

Comments
 (0)