Skip to content
7 changes: 7 additions & 0 deletions .gitignore
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
Copy link
Owner

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

.venv
4 changes: 2 additions & 2 deletions skpy/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def fromRaw(cls, skype=None, raw={}):
active = True
try:
info = skype.conn("GET", "{0}/threads/{1}".format(skype.conn.msgsHost, raw.get("id")),
auth=SkypeConnection.Auth.RegToken,
auth=SkypeConnection.Auth.SkypeToken,
params={"view": "msnp24Equivalent"}).json()
except SkypeApiException as e:
if e.args[1].status_code in (400, 403, 404):
Expand Down Expand Up @@ -491,7 +491,7 @@ def recent(self):
"view": "supportsExtendedHistory|msnp24Equivalent",
"targetType": "Passport|Skype|Lync|Thread|Agent|ShortCircuit|PSTN|Flxt|NotificationStream|"
"ModernBots|secureThreads|InviteFree"}
resp = self.skype.conn.syncStateCall("GET", url, params, auth=SkypeConnection.Auth.RegToken).json()
resp = self.skype.conn.syncStateCall("GET", url, params, auth=SkypeConnection.Auth.SkypeToken).json()
chats = {}
for json in resp.get("conversations", []):
chat = SkypeChat.fromRaw(self.skype, json)
Expand Down
109 changes: 70 additions & 39 deletions skpy/conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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):
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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)))
Expand All @@ -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):
"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be better to name like _initializeRegTokenFromHeader

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_extractRegistrationTokenFromResponse

_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.
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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,
regExpiry,
self.msgsHost
]) + "\n"

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_extractRegistrationToken doesn't exist yet here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Owner

Choose a reason for hiding this comment

The 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

yes, but it didn't exist in this commit yet :)


Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think better to do something like NotImplementedError

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Owner

Choose a reason for hiding this comment

The 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):
"""
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion skpy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ def subscribePresence(self):
"""
self.conn.endpoints["self"].subscribePresence(self.contacts)

@SkypeConnection.handle(404, regToken=True)
@SkypeConnection.handle(404, subscribe="self")
def getEvents(self):
"""
Expand Down
28 changes: 28 additions & 0 deletions test/API_FIXES_TODO.md
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
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check testContacts server test

- **Error**: User lookup returns `None`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just checked, contacts = myteams.contacts works fine, the same as contacts['teamsid']. Can you clearlify a bit, what is not working?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 testContacts server test

- **Fix**: Update contact search endpoints

Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

myteams.settings works fine, but indeed even ping options.skype.com not works. And I see that it has influence just on callPrivacy and callPrivacyOpt from what myteams.settings returns. But generally, as I understand, this endpoint is not needed yet, because everything is ruled by https://account.microsoft.com/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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`
Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

@mihalt mihalt Aug 6, 2025

Choose a reason for hiding this comment

The 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': '',
'BehaviorOverride': 'redirectAs404',
'RegistrationToken': '
',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'X-SkypeToken': '***'}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The 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 skpy.core.SkypeRateLimitException: ('Rate limit exceeded', <Response [429]>) even with first iteration of .recent(). I think the general limitations of teams are much less now then in skype and we should take into accounting this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Owner

Choose a reason for hiding this comment

The 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.

Loading