Ho ho ho says Santa! But today also me!
A lot has been going on since my last post. A lot! This is the first article with a little gift to the community! Since we started on Snips we’ve all tried to programmatically train and download our assistants. Project Alice did it, in a way that really wasn’t convenient:
- Hey Alice!
- Yes?
- Update yourself
- Ok -> Stopping Snips services, downloading the new assistant I had previously uploaded to my git repo, extracting it, place it where it belongs and restarting snips
Of course this worked but I had to manually train and download the assistant from the console and upload it to my git repo…
Then came the browser way. We can mimic the user activity on the browser, but somehow, at some point, we couldn’t train our assistant anymore, leaving us with login to console to train it before downloading it anyway…
Then came SAM, the Snips tool to manage your device and assistants/skills. Sam can train and download the assistant, provided your Snips account credentials. So if Sam can, why can’t we?
Disclaimer
- Is this hacking?
- No, it’s not, you only gain access to what’s rightfully yours. It’s more some reverse engineering and basic comprehension
- Is this dangerous?
- I won’t share any destructive endpoints here, but potentially you could end up deleting your assistant
- Is this allowed?
- I did ask the permission to share, wasn’t answered. I can’t see why it wouldn’t be though, as again, you only access your data
- Is it hard?
- No, not at all, the script is very short
- I do not take any responsability if anything bad happens, data loss, key stolen, ban, beer spilled on keyboard etc.
JWT

Stands for Json Web token and is a way to authenticate a user to righfully access some locked data by providing credentials only once. You use them daily in your browsers without even knowing it. The JWT token is composed of three parts, separated by dots that are commonly base64 encoded. The first part contains the token infos (encryption used), the second part contains the payload, whatever needs to be passed and the third part contains the signature. How does it work for Snips? It’s pretty simple:
- User asks the server to login, sending login and password on a TLS encrypted channel
- Server authentifies the user and sends a short life JWT token back to user
- User creates an alias and sends it to the server along with the JWT token provided just before
- Server checks the JWT token and the alias and sends a master JWT token back to user
- User stores the master JWT token and alias for any further connection to server
What’s needed?
I’ll show the way using python and will share a working copy of the script at the end of this article. Use python 3, even thoug it’s not essential, but python 2 is at its end of life. The only dependency you will need is Requestspip3 install requests
Let’s do this!
This is a very basic and quick written script, for demo purpose only. You will need to make it stronger and add more checks! Create your python script, call it whatever you want.
# -*- coding: utf-8 -*- import json import requests import toml import uuid class SnipsConsole: EMAIL = 'john' PASSWORD = 'doe' def __init__(self): self._tries = 0 self._connected = False self._headers = { 'Accept' : 'application/json', 'Content-Type': 'application/json' } with open('/etc/snips.toml') as f: self._snips = toml.load(f) if 'console' in self._snips and 'console_token' in self._snips['console']: self._headers['Authorization'] = 'JWT {}'.format(self._snips['console']['console_token']) self._connected = True else: self._login()
- I do set a max try to 3 so in case of failure we can retry but not endlessly
- Next comes the basic header definition
- I use snips.toml to store the information, so everything to do with snips stays at the same place and load it with the “toml” module
pip3 install toml
- If the token is already in the configurations we expend the header with the authorization token, if not, we call the login function
def _login(self): self._tries += 1 if self._tries > 3: print("Max login tries reached, aborting") self._tries = 0 return payload = { 'email': self.EMAIL, 'password': self.PASSWORD } req = self._req(url='v1/user/auth', data=payload) if req.status_code == 200: print('Connected to snips account, fetching auth token') try: token = req.headers['authorization'] user = User(json.loads(req.content)['user']) accessToken = self._getAccessToken(user, token) if len(accessToken) > 0: print('Console token aquired, saving it!') if 'console' not in self._snips: self._snips['console'] = {} self._snips['console']['console_token'] = accessToken['token'] self._snips['console']['console_alias'] = accessToken['alias'] self._headers['Authorization'] = 'JWT {}'.format(accessToken['token']) self._saveSnipsConf() self._connected = True self._tries = 0 except Exception as e: print('Exception during console token aquiring: {}'.format(e)) self._connected = False return else: print("Couldn't connect to console: {}".format(req.status_code)) self._connected = False
- We first check if we have exceeded our tries, if yes we just stop
- We prepare the payload with the needed informations, email and password of your Snips console account
- We try to connect, using a function declared later, pointing to v1/user/auth with the payload previously declared
- If the server answers with the http status code 200 it means we’ve been accepted otherwise the account connection failed and we can’t go further
- We fetch the pre auth token that is passed by the server back to us in the response header
- We build a User class
- We fetch the console access token
- If we get a console access token, we save it and load it in our headers for further user/passwordless communication
- We set our state to connected and clear the tries if we ever need to go through the process again
def _getAccessToken(self, user, token: str) -> dict: alias = 'sam-{}'.format(str(uuid.uuid4())).replace('-', '')[:29] self._headers['Authorization'] = token req = self._req(url='v1/user/{}/accesstoken'.format(user.userId), data={'alias': alias}) if req.status_code == 201: return json.loads(req.content)['token'] return {}
- We need to define an alias for the token. This is made by generating a uuid version 4 appended to the string “sam-“. We get rid of any “-” in that string and use only the first 29 characters. Don’t ask why, it’s that way. You can replace “sam-” with anything. I use “projectalice-“
- We use the pre auth token we got in our headers so the server knows it’s us.
- We send the request to the endpoint “v1/user/USERID/accesstoken“. USERID comes from the previous request, when we built the “User” class
- If the server responds with the http code “201” we’ve been accepted and we return a dict made out of the “token” part of the response content
def _saveSnipsConf(self): with open('/etc/snips.toml', 'w') as f: toml.dump(self._snips, f)
- Quick function to save our settings to snips.toml
def _req(self, url: str='', method: str='post', data: dict=None, **kwargs) -> requests.Response: req = requests.request(method=method, url='https://external-gateway.snips.ai/{}'.format(url), json=data, headers=self._headers, **kwargs) if req.status_code == 401: print('Console token expired or refused, need to login again') if 'Authorization' in self._headers: del self._headers['Authorization'] self._connected = False self._snips['console']['console_token'] = '' self._snips['console']['console_alias'] = '' self._saveSnipsConf() self._login() return req
- The reason why I made this _req function instead of directly using requests built in functions is that if for any query to the snips server we make we get a 401 status code, the token has a problem and we need to call the login function again. Instead of checking the status after every http call I made one function for all the calls, that does the checking part
- We send the request to the server by appending the passed url to the url base which is
https://external-gateway.snips.ai
, passing the headers and the payload as well as any other accepted arguments (**kwargs) - If we get a “401” http status code back, the token has been refused in which case we delete the authorization header, get rid of the snips toml configuration and call the login function again. Now the self._tries surely makes sense?
class User: def __init__(self, data): self._userId = data['id'] self._userEmail = data['email'] @property def userId(self) -> str: return self._userId
- A simple class to hold the userid and the user email
That’s it!!
Yep, we’ve done it! We are connected to the snips server and we can try different endpoints, like listing our assistants, skills, train the nlu or asr, download the assistant zip file etc 🙂 Let me give you a few non destructive endpoints. They all need the ‘Authorization’ header to be set with the JWT key to be reachable!
- NLU status: /v3/assistant/ASSISTANT_ID/status (method ‘get’) => where ASSISTANT_ID is replaced by the id of the wanted assistant.
- NLU training: /v1/training (method ‘post’) => data: ‘assistantId’
- ASR status: /v1/languagemodel/status (method ‘get’) => data: ‘assistantId’.
- ASR training: /v1/languagemodel (method ‘post’) => data: ‘assistantId’
- Assistant listing: /v3/assistant (method ‘get’) => data: ‘userId’
- Assistant download: /v3/assistant/ASSISTANT_ID/download (method ‘get’) => where ASSISTANT_ID is replaced by the id of the wanted assistant.
- Logout: /v1/user/USER_ID/accesstoken/ALIAS (method ‘get’). This deletes the alias and token from snips server!
That’s about enough for today! I hope you enjoyed this little introduction to how to programmatically manage your assistant in python! As always, dev safe!
Full working copy
Here’s the link: https://github.com/Psychokiller1888/snipsSamless/blob/master/main.py
Make sure to have the dependencies installed (toml and requests) and to run python 3. Run the script. Type email and enter the email. Type password and enter your password. Type login to log into the console and test the functions!
Links
- Snips: https://snips.ai
- Twitter: https://twitter.com/Psychokiller188
- Github: https://github.com/Psychokiller1888
- This code: https://github.com/Psychokiller1888/snipsSamless