-
Notifications
You must be signed in to change notification settings - Fork 0
Use SkypeToken authentication in chat API. #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9eb861b
a26b192
70aa69e
b379000
24b99de
15d424c
3b5b7ac
7c7e949
d9c674a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Byte-compiled / optimized / DLL files | ||
| __pycache__/ | ||
| *.py[cod] | ||
|
|
||
| # Virtual environment | ||
| .env | ||
| .venv | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,12 +62,11 @@ def handle(*codes, **kwargs): | |
|
|
||
| Args: | ||
| codes (int list): status codes to respond to | ||
| regToken (bool): whether to try retrieving a new token on error | ||
| subscribe (str): endpoint to subscribe if needed | ||
|
|
||
| Returns: | ||
| method: decorator function, ready to apply to other methods | ||
| """ | ||
| regToken = kwargs.get("regToken", False) | ||
| subscribe = kwargs.get("subscribe") | ||
|
|
||
| def decorator(fn): | ||
|
|
@@ -78,8 +77,6 @@ def wrapper(self, *args, **kwargs): | |
| except SkypeApiException as e: | ||
| if isinstance(e.args[1], requests.Response) and e.args[1].status_code in codes: | ||
| conn = self if isinstance(self, SkypeConnection) else self.conn | ||
| if regToken: | ||
| conn.getRegToken() | ||
| if subscribe: | ||
| conn.endpoints[subscribe].subscribe() | ||
| return fn(self, *args, **kwargs) | ||
|
|
@@ -141,7 +138,7 @@ def externalCall(cls, method, url, codes=(200, 201, 204, 207), **kwargs): | |
| API_ASM_LOCAL = "https://{0}1-api.asm.skype.com/v1/objects" | ||
| API_URL = "https://urlp.asm.skype.com/v1/url/info" | ||
| API_CONTACTS = "https://contacts.skype.com/contacts/v2" | ||
| API_MSGSHOST = "https://client-s.gateway.messenger.live.com/v1" | ||
| API_MSGSHOST = "https://teams.live.com/api/chatsvc/consumer/v1" | ||
| API_DIRECTORY = "https://skypegraph.skype.com/v2.0/search/" | ||
| # Version doesn't seem to be important, at least not for what we need. | ||
| API_CONFIG = "https://a.config.skype.com/config/v1" | ||
|
|
@@ -155,6 +152,8 @@ def externalCall(cls, method, url, codes=(200, 201, 204, 207), **kwargs): | |
|
|
||
| extSess = requests.Session() | ||
| extSess.headers["User-Agent"] = USER_AGENT | ||
| extSess.headers["ms-ic3-additional-product"] = "Sfl" | ||
| extSess.headers["ms-ic3-product"] = "tfl" | ||
|
|
||
| def __init__(self): | ||
| """ | ||
|
|
@@ -168,13 +167,14 @@ def __init__(self): | |
| self.msgsHost = self.API_MSGSHOST | ||
| self.sess = requests.Session() | ||
| self.sess.headers["User-Agent"] = self.USER_AGENT | ||
| self.sess.headers["ms-ic3-additional-product"] = "Sfl" | ||
| self.sess.headers["ms-ic3-product"] = "tfl" | ||
| self.endpoints = {"self": SkypeEndpoint(self, "SELF")} | ||
| self.syncStates = {} | ||
|
|
||
| @property | ||
| def connected(self): | ||
| return "skype" in self.tokenExpiry and datetime.now() <= self.tokenExpiry["skype"] \ | ||
| and "reg" in self.tokenExpiry and datetime.now() <= self.tokenExpiry["reg"] | ||
| return "skype" in self.tokenExpiry and datetime.now() <= self.tokenExpiry["skype"] | ||
|
|
||
| @property | ||
| def guest(self): | ||
|
|
@@ -224,19 +224,34 @@ def __call__(self, method, url, codes=(200, 201, 202, 204, 207), auth=None, head | |
| if not headers: | ||
| headers = {} | ||
| debugHeaders = dict(headers) | ||
| if auth == self.Auth.SkypeToken: | ||
| headers["x-ms-client-consumer-type"] = "teams4life" | ||
| headers["ms-ic3-additional-product"] = "Sfl" | ||
| headers["ms-ic3-product"] = "tfl" | ||
|
|
||
| # Always include Skype token when available | ||
| if self.tokens.get("skype"): | ||
| headers["X-SkypeToken"] = self.tokens["skype"] | ||
| headers["Authentication"] = "skypetoken=" + self.tokens["skype"] | ||
| debugHeaders["X-SkypeToken"] = "***" | ||
| elif auth == self.Auth.Authorize: | ||
| headers["Authorization"] = "skype_token {0}".format(self.tokens["skype"]) | ||
| debugHeaders["Authorization"] = "***" | ||
| elif auth == self.Auth.RegToken: | ||
| debugHeaders["Authentication"] = "***" | ||
|
|
||
| # Always include registration token when available | ||
| if self.tokens.get("reg"): | ||
| headers["RegistrationToken"] = self.tokens["reg"] | ||
| debugHeaders["RegistrationToken"] = "***" | ||
|
|
||
| if auth == self.Auth.Authorize: | ||
| headers["Authorization"] = "skype_token {0}".format(self.tokens["skype"]) | ||
| debugHeaders["Authorization"] = "***" | ||
|
|
||
| if os.getenv("SKPY_DEBUG_HTTP"): | ||
| print("<= [{0}] {1} {2}".format(datetime.now().strftime("%d/%m %H:%M:%S"), method, url)) | ||
| print(pformat(dict(kwargs, headers=debugHeaders))) | ||
| resp = self.sess.request(method, url, headers=headers, **kwargs) | ||
|
|
||
| # Extract registration token from response headers if present | ||
| self._extractRegistrationToken(resp) | ||
|
|
||
| if os.getenv("SKPY_DEBUG_HTTP"): | ||
| print("=> [{0}] {1}".format(datetime.now().strftime("%d/%m %H:%M:%S"), resp.status_code)) | ||
| print(pformat(dict(resp.headers))) | ||
|
|
@@ -250,6 +265,30 @@ def __call__(self, method, url, codes=(200, 201, 202, 204, 207), auth=None, head | |
| raise SkypeApiException("{0} response from {1} {2}".format(resp.status_code, method, url), resp) | ||
| return resp | ||
|
|
||
| def _extractRegistrationToken(self, resp): | ||
| """ | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think would be better to name like _initializeRegTokenFromHeader
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. init is not correct here because reg token is extracted from each appropriate request and is updated if needed, FromHeader is also not describing properly what parameters are passed and what headers to use, thus better name can be _extractRegistrationTokenFromResponse or similar
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
_updateRegistrationTokenFromResponse. The problem here is that not clear that something happens after token extraction. |
||
| Extract registration token from response headers if present. | ||
|
|
||
| Args: | ||
| resp (requests.Response): HTTP response to check for registration token | ||
| """ | ||
| regTokenHead = resp.headers.get("Set-RegistrationToken") | ||
| if regTokenHead: | ||
| import re | ||
| tokenMatch = re.search(r"registrationToken=([a-z0-9\+/=]+)", regTokenHead, re.I) | ||
| expiryMatch = re.search(r"expires=(\d+)", regTokenHead) | ||
| if tokenMatch: | ||
| self.tokens["reg"] = tokenMatch.group(1) | ||
| if expiryMatch: | ||
| self.tokenExpiry["reg"] = datetime.fromtimestamp(int(expiryMatch.group(1))) | ||
| else: | ||
| # Default expiry if not specified (24 hours from now) | ||
| self.tokenExpiry["reg"] = datetime.now() + timedelta(hours=24) | ||
|
|
||
| # Update token file if configured | ||
| if self.tokenFile and "skype" in self.tokens: | ||
| self.writeToken() | ||
|
|
||
| def syncStateCall(self, method, url, params={}, **kwargs): | ||
| """ | ||
| Follow and track sync state URLs provided by an API endpoint, in order to implicitly handle pagination. | ||
|
|
@@ -321,8 +360,6 @@ def readTokenFromStr(self, tokens): | |
| self.tokens["reg"] = regToken | ||
| self.tokenExpiry["reg"] = regExpiry | ||
| self.msgsHost = msgsHost | ||
| else: | ||
| self.getRegToken() | ||
|
|
||
| def readToken(self): | ||
| """ | ||
|
|
@@ -351,12 +388,18 @@ def writeTokenToStr(self): | |
| Returns: | ||
| str: A token string that can be used by :meth:`readTokenFromStr` to re-authenticate. | ||
| """ | ||
| # Use empty string and 0 expiry if registration token is not available yet | ||
| regToken = self.tokens.get("reg", "") | ||
| regExpiry = "" | ||
| if "reg" in self.tokenExpiry: | ||
| regExpiry = str(int(time.mktime(self.tokenExpiry["reg"].timetuple()))) | ||
|
|
||
| return "\n".join([ | ||
| self.userId, | ||
| self.tokens["skype"], | ||
| str(int(time.mktime(self.tokenExpiry["skype"].timetuple()))), | ||
| self.tokens["reg"], | ||
| str(int(time.mktime(self.tokenExpiry["reg"].timetuple()))), | ||
| regToken, | ||
mihalt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| regExpiry, | ||
| self.msgsHost | ||
| ]) + "\n" | ||
|
|
||
|
|
@@ -387,9 +430,6 @@ def verifyToken(self, auth): | |
| if not hasattr(self, "getSkypeToken"): | ||
| raise SkypeTokenException("Skype token expired, and no password specified") | ||
| self.getSkypeToken() | ||
| elif auth == self.Auth.RegToken: | ||
| if "reg" not in self.tokenExpiry or datetime.now() >= self.tokenExpiry["reg"]: | ||
| self.getRegToken() | ||
|
|
||
| def skypeTokenClosure(self, method, *args, **kwargs): | ||
| """ | ||
|
|
@@ -435,7 +475,6 @@ def liveLogin(self, user, pwd): | |
| self.skypeTokenClosure(self.liveLogin, user, pwd) | ||
| self.tokens["skype"], self.tokenExpiry["skype"] = SkypeLiveAuthProvider(self).auth(user, pwd) | ||
| self.getUserId() | ||
| self.getRegToken() | ||
|
|
||
| def soapLogin(self, user, pwd): | ||
| """ | ||
|
|
@@ -462,7 +501,6 @@ def soapLogin(self, user, pwd): | |
| self.skypeTokenClosure(self.soapLogin, user, pwd) | ||
| self.tokens["skype"], self.tokenExpiry["skype"] = SkypeSOAPAuthProvider(self).auth(user, pwd) | ||
| self.getUserId() | ||
| self.getRegToken() | ||
|
|
||
| def guestLogin(self, url, name): | ||
| """ | ||
|
|
@@ -481,7 +519,6 @@ def guestLogin(self, url, name): | |
| """ | ||
| self.tokens["skype"], self.tokenExpiry["skype"] = SkypeGuestAuthProvider(self).auth(url, name) | ||
| self.getUserId() | ||
| self.getRegToken() | ||
|
|
||
| def getSkypeToken(self): | ||
| """ | ||
|
|
@@ -501,7 +538,6 @@ def refreshSkypeToken(self): | |
| .SkypeApiException: if the login form can't be processed | ||
| """ | ||
| self.tokens["skype"], self.tokenExpiry["skype"] = SkypeRefreshAuthProvider(self).auth(self.tokens["skype"]) | ||
| self.getRegToken() | ||
|
|
||
| def getUserId(self): | ||
| """ | ||
|
|
@@ -512,21 +548,12 @@ def getUserId(self): | |
|
|
||
| def getRegToken(self): | ||
| """ | ||
| Acquire a new registration token. | ||
|
|
||
| Once successful, all tokens and expiry times are written to the token file (if specified on initialisation). | ||
| Registration tokens are now obtained passively from server responses. | ||
| This method is kept for backward compatibility but no longer performs explicit requests. | ||
| """ | ||
| self.verifyToken(self.Auth.SkypeToken) | ||
| token, expiry, msgsHost, endpoint = SkypeRegistrationTokenProvider(self).auth(self.tokens["skype"]) | ||
| self.tokens["reg"] = token | ||
| self.tokenExpiry["reg"] = expiry | ||
| self.msgsHost = msgsHost | ||
| if endpoint: | ||
| endpoint.config() | ||
| self.endpoints["main"] = endpoint | ||
| self.syncEndpoints() | ||
| if self.tokenFile: | ||
| self.writeToken() | ||
| # No-op: Registration tokens are now extracted from Set-RegistrationToken headers | ||
| # in all HTTP responses via _extractRegistrationToken() | ||
| pass | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _extractRegistrationToken doesn't exist yet here
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _extractRegistrationToken is in same file, so it is correct to reference it here
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes, but it didn't exist in this commit yet :) |
||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think better to do something like NotImplementedError
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From backward compatibility it is not, better to have it undefined, it won't introduce breaking chages. I future it should just be removed, but on this stage looks ok
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you mean the legacy code with usage of previous versions skpy by users? How will they understand then, that this is not supported place that should be changed? If it just returns None, it can be a suffer to debug. |
||
| def syncEndpoints(self): | ||
| """ | ||
|
|
@@ -924,12 +951,16 @@ def getToken(self, t): | |
|
|
||
| class SkypeRegistrationTokenProvider(SkypeAuthProvider): | ||
| """ | ||
| An authentication provider that handles the handshake for a registration token. | ||
| DEPRECATED: This class is no longer used for active token requests. | ||
| Registration tokens are now obtained passively from Set-RegistrationToken headers. | ||
|
|
||
| Kept for backward compatibility but should not be used directly. | ||
| """ | ||
|
|
||
| def auth(self, skypeToken): | ||
| """ | ||
| Request a new registration token using a current Skype token. | ||
| DEPRECATED: This method is no longer used. | ||
| Registration tokens are now extracted automatically from server responses. | ||
|
|
||
| Args: | ||
| skypeToken (str): existing Skype token | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # API Fixes TODO | ||
|
|
||
| ## ✅ Working | ||
| - Authentication ✅ | ||
| - User profile ✅ | ||
| - Services ✅ | ||
|
|
||
| ## ❌ Need Fixes | ||
|
|
||
| ### 1. Contacts API | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not 100% sure, but contacts.contactIds can contain just old skype accounts, but not new. At least I haven't seen emails at first glance.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check |
||
| - **Error**: User lookup returns `None` | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just checked,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All notes here are related to server test expectations, in this particular case user lookup is not working, you can run |
||
| - **Fix**: Update contact search endpoints | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the search is indeed not working. The url have the format now like https://presence.teams.live.com/v1/pubsub/subscriptions/{x-ms-endpoint-id}. So, I think that header x-ms-endpoint-id is connected with subscriptions and it should be implemented first. |
||
| ### 2. Settings API | ||
| - **Error**: DNS failure for `options.skype.com` | ||
| - **Fix**: Find new settings endpoint URL | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. myteams.settings works fine, but indeed even
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API can be deprecated if it is not needed |
||
|
|
||
| ### 3. Translation API | ||
| - **Error**: `404 from dev.microsofttranslator.com` | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the less important one :) |
||
| - **Fix**: Update translator API endpoint | ||
|
|
||
| ### 4. Subscriptions API | ||
| - **Error**: `410 Gone` from event subscriptions | ||
| - **Fix**: Investigate new subscription method | ||
|
|
||
| ### 5. Chat API | ||
| - **Error**: `401/404` from conversation endpoints | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my_contact_chat.getMsgs() the same as .sendMsg() - I see the error 401 and the header 'StatusText': 'Invalid registration token header.'. Probably is not everything fine with current auth implementation. This tokens were on outcomming request to https://teams.live.com/api/chatsvc/consumer/v1/users/ME/conversations/8:my_contact_id/messages : {'Authentication': '',
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please describe test case more in detail, what was the token state at that stage, when it is reproduced, etc
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, I am sorry, looks like that it is github formatting. There are *** in Authentication, Registrationtoken. So, the values are appeared. I see them in debuger too. |
||
| - **Fix**: Check conversation ID format changes | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that everything is fine in .recent() call now. Just one problem is with raised
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please describe steps to reproduce it in detail as well, do you use parallel calls, is registration token acquired and passed to the request, etc
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Nothing unusual, reg token is available, how to test are calls parallel? Just call myteams.chats.recent(). But there are a lot of chats in my personal account. I think, that's why a lot of requests happen and raise this error. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is not python-dotenv here. I think, should be deleted