Skip to content

Commit 0651078

Browse files
authored
Merge pull request #8 from Linusp/dev
Use OAuth2
2 parents c1391ee + d6a8de7 commit 0651078

File tree

8 files changed

+322
-175
lines changed

8 files changed

+322
-175
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# CHANGELOG
22

3+
4+
## v0.4.0
5+
6+
Added
7+
8+
- New Class: `InoreaderConfigManager` for config management
9+
10+
Changed
11+
12+
- Use OAuth2.0 authentication instead of user authentication with password
13+
- Optimized code of `InoreaderClient`
14+
- Optimized results of commands
15+
316
## v0.3.0
417

518
Added
@@ -35,7 +48,7 @@ Added
3548
- `InoreaderClient.mark_as_read`
3649
- `InoreaderClient.mark_as_starred`
3750
- `InoreaderClient.mark_as_liked`
38-
- `InoreaderClient.boradcast`
51+
- `InoreaderClient.broadcast`
3952

4053
- New command `filter`
4154

README.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,10 @@ pip install git+https://github.com/Linusp/python-inoreader.git
2121

2222
## Usage
2323

24-
1. [Register your application](https://www.inoreader.com/developers/register-app)
25-
26-
2. Set `appid` and `appkey` in your system, you can set them with environment variables like
27-
28-
```shell
29-
export INOREADER_APP_ID = 'your-app-id'
30-
export INOREADER_APP_KEY = 'your-app-key'
31-
```
32-
33-
or write them in `$HOME/.inoreader`, e.g.:
34-
```shell
35-
[auth]
36-
appid = your-app-id
37-
appkey = your-app-key
38-
```
39-
40-
3. Login to your Inoreader account
24+
1. Login to your Inoreader account
4125

4226
```shell
4327
inoreader login
4428
```
4529

46-
3. Use the command line tool `inoreader` to do something, run `inoreader --help` for details
30+
2. Use the command line tool `inoreader` to do something, run `inoreader --help` for details

inoreader/client.py

Lines changed: 100 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# coding: utf-8
22
from __future__ import print_function, unicode_literals
33

4+
import logging
45
from uuid import uuid4
6+
from datetime import datetime
57
from operator import itemgetter
68
try: # python2
79
from urlparse import urljoin
@@ -11,63 +13,96 @@
1113

1214
import requests
1315

14-
from .consts import BASE_URL, LOGIN_URL
16+
from .consts import BASE_URL
1517
from .exception import NotLoginError, APIError
1618
from .article import Article
1719
from .subscription import Subscription
1820

1921

22+
LOGGER = logging.getLogger(__name__)
23+
24+
2025
class InoreaderClient(object):
2126

22-
def __init__(self, app_id, app_key, userid=None, auth_token=None):
27+
# paths
28+
TOKEN_PATH = '/oauth2/token'
29+
USER_INFO_PATH = 'user-info'
30+
TAG_LIST_PATH = 'tag/list'
31+
SUBSCRIPTION_LIST_PATH = 'subscription/list'
32+
STREAM_CONTENTS_PATH = 'stream/contents/'
33+
EDIT_TAG_PATH = 'edit-tag'
34+
35+
# tags
36+
GENERAL_TAG_TEMPLATE = 'user/-/label/{}'
37+
READ_TAG = 'user/-/state/com.google/read'
38+
STARRED_TAG = 'user/-/state/com.google/starred'
39+
LIKED_TAG = 'user/-/state/com.google/like'
40+
BROADCAST_TAG = 'user/-/state/com.google/broadcast'
41+
42+
def __init__(self, app_id, app_key, access_token, refresh_token,
43+
expires_at, config_manager=None):
2344
self.app_id = app_id
2445
self.app_key = app_key
25-
self.auth_token = auth_token
46+
self.access_token = access_token
47+
self.refresh_token = refresh_token
48+
self.expires_at = float(expires_at)
2649
self.session = requests.Session()
2750
self.session.headers.update({
2851
'AppId': self.app_id,
2952
'AppKey': self.app_key,
30-
'Authorization': 'GoogleLogin auth={}'.format(self.auth_token)
53+
'Authorization': 'Bearer {}'.format(self.access_token)
3154
})
32-
if userid:
33-
self.userid = userid
34-
else:
35-
self.userid = None if not self.auth_token else self.userinfo()['userId']
36-
37-
def userinfo(self):
38-
if not self.auth_token:
39-
raise NotLoginError
55+
self.config_manager = config_manager
4056

41-
url = urljoin(BASE_URL, 'user-info')
42-
resp = self.session.post(url)
43-
if resp.status_code != 200:
44-
raise APIError(resp.text)
57+
def check_token(self):
58+
now = datetime.now().timestamp()
59+
if now >= self.expires_at:
60+
self.refresh_access_token()
4561

46-
return resp.json()
47-
48-
def login(self, username, password):
49-
resp = self.session.get(LOGIN_URL, params={'Email': username, 'Passwd': password})
50-
if resp.status_code != 200:
51-
return False
62+
@staticmethod
63+
def parse_response(response, json_data=True):
64+
if response.status_code == 401:
65+
raise NotLoginError
66+
elif response.status_code != 200:
67+
raise APIError(response.text)
68+
69+
return response.json() if json_data else response.text
70+
71+
def refresh_access_token(self):
72+
url = urljoin(BASE_URL, self.TOKEN_PATH)
73+
payload = {
74+
'client_id': self.app_id,
75+
'client_secret': self.app_key,
76+
'grant_type': 'refresh_token',
77+
'refresh_token': self.refresh_token,
78+
}
79+
response = self.parse_response(requests.post(url, json=payload))
80+
self.access_token = response['access_token']
81+
self.refresh_token = response['refresh_token']
82+
self.expires_at = datetime.now().timestamp() + response['expires_in']
83+
self.session.headers['Authorization'] = 'Bear {}'.format(self.access_token)
84+
85+
if self.config_manager:
86+
self.config_manager.access_token = self.access_token
87+
self.config_manager.refresh_token = self.refresh_token
88+
self.config_manager.expires_at = self.expires_at
89+
self.config_manager.save()
5290

53-
for line in resp.text.split('\n'):
54-
if line.startswith('Auth'):
55-
self.auth_token = line.replace('Auth=', '').strip()
91+
def userinfo(self):
92+
self.check_token()
5693

57-
return bool(self.auth_token)
94+
url = urljoin(BASE_URL, self.USER_INFO_PATH)
95+
return self.parse_response(self.session.post(url))
5896

5997
def get_folders(self):
60-
if not self.auth_token:
61-
raise NotLoginError
98+
self.check_token()
6299

63-
url = urljoin(BASE_URL, 'tag/list')
100+
url = urljoin(BASE_URL, self.TAG_LIST_PATH)
64101
params = {'types': 1, 'counts': 1}
65-
resp = self.session.post(url, params=params)
66-
if resp.status_code != 200:
67-
raise APIError(resp.text)
102+
response = self.parse_response(self.session.post(url, params=params))
68103

69104
folders = []
70-
for item in resp.json()['tags']:
105+
for item in response['tags']:
71106
if item.get('type') != 'folder':
72107
continue
73108

@@ -78,17 +113,14 @@ def get_folders(self):
78113
return folders
79114

80115
def get_tags(self):
81-
if not self.auth_token:
82-
raise NotLoginError
116+
self.check_token()
83117

84-
url = urljoin(BASE_URL, 'tag/list')
118+
url = urljoin(BASE_URL, self.TAG_LIST_PATH)
85119
params = {'types': 1, 'counts': 1}
86-
resp = self.session.post(url, params=params)
87-
if resp.status_code != 200:
88-
raise APIError(resp.text)
120+
response = self.parse_response(self.session.post(url, params=params))
89121

90122
tags = []
91-
for item in resp.json()['tags']:
123+
for item in response['tags']:
92124
if item.get('type') != 'tag':
93125
continue
94126

@@ -99,15 +131,11 @@ def get_tags(self):
99131
return tags
100132

101133
def get_subscription_list(self):
102-
if not self.auth_token:
103-
raise NotLoginError
104-
105-
url = urljoin(BASE_URL, 'subscription/list')
106-
resp = self.session.get(url)
107-
if resp.status_code != 200:
108-
raise APIError(resp.text)
134+
self.check_token()
109135

110-
for item in resp.json()['subscriptions']:
136+
url = urljoin(BASE_URL, self.SUBSCRIPTION_LIST_PATH)
137+
response = self.parse_response(self.session.get(url))
138+
for item in response['subscriptions']:
111139
yield Subscription.from_json(item)
112140

113141
def get_stream_contents(self, stream_id, c=''):
@@ -119,45 +147,34 @@ def get_stream_contents(self, stream_id, c=''):
119147
break
120148

121149
def __get_stream_contents(self, stream_id, continuation=''):
122-
if not self.auth_token:
123-
raise NotLoginError
150+
self.check_token()
124151

125-
url = urljoin(BASE_URL, 'stream/contents/' + quote_plus(stream_id))
152+
url = urljoin(BASE_URL, self.STREAM_CONTENTS_PATH + quote_plus(stream_id))
126153
params = {
127154
'n': 50, # default 20, max 1000
128155
'r': '',
129156
'c': continuation,
130157
'output': 'json'
131158
}
132-
resp = self.session.post(url, params=params)
133-
if resp.status_code != 200:
134-
raise APIError(resp.text)
135-
136-
if 'continuation' in resp.json():
137-
return resp.json()['items'], resp.json()['continuation']
159+
response = self.parse_response(self.session.post(url, params=params))
160+
if 'continuation' in response():
161+
return response['items'], response['continuation']
138162
else:
139-
return resp.json()['items'], None
163+
return response['items'], None
140164

141165
def fetch_unread(self, folder=None, tags=None):
142-
if not self.auth_token:
143-
raise NotLoginError
166+
self.check_token()
144167

145-
url = urljoin(BASE_URL, 'stream/contents/')
168+
url = urljoin(BASE_URL, self.STREAM_CONTENTS_PATH)
146169
if folder:
147170
url = urljoin(
148171
url,
149-
quote_plus('user/{}/label/{}'.format(self.userid, folder))
172+
quote_plus(self.GENERAL_TAG_TEMPLATE.format(folder))
150173
)
151-
params = {
152-
'xt': 'user/{}/state/com.google/read'.format(self.userid),
153-
'c': str(uuid4())
154-
}
174+
params = {'xt': self.READ_TAG, 'c': str(uuid4())}
155175

156-
resp = self.session.post(url, params=params)
157-
if resp.status_code != 200:
158-
raise APIError(resp.text)
159-
160-
for data in resp.json()['items']:
176+
response = self.parse_response(self.session.post(url, params=params))
177+
for data in response['items']:
161178
categories = set([
162179
category.split('/')[-1] for category in data.get('categories', [])
163180
if category.find('label') > 0
@@ -166,48 +183,43 @@ def fetch_unread(self, folder=None, tags=None):
166183
continue
167184
yield Article.from_json(data)
168185

169-
continuation = resp.json().get('continuation')
186+
continuation = response.get('continuation')
170187
while continuation:
171188
params['c'] = continuation
172-
resp = self.session.post(url, params=params)
173-
if resp.status_code != 200:
174-
raise APIError(resp.text)
175-
for data in resp.json()['items']:
189+
response = self.parse_response(self.session.post(url, params=params))
190+
for data in response['items']:
176191
categories = set([
177192
category.split('/')[-1] for category in data.get('categories', [])
178193
if category.find('label') > 0
179194
])
180195
if tags and not categories.issuperset(set(tags)):
181196
continue
182197
yield Article.from_json(data)
183-
continuation = resp.json().get('continuation')
198+
continuation = response.get('continuation')
184199

185200
def add_general_label(self, articles, label):
186-
if not self.auth_token:
187-
raise NotLoginError
201+
self.check_token()
188202

189-
url = urljoin(BASE_URL, 'edit-tag')
203+
url = urljoin(BASE_URL, self.EDIT_TAG_PATH)
190204
for start in range(0, len(articles), 10):
191205
end = min(start + 10, len(articles))
192206
params = {
193207
'a': label,
194208
'i': [articles[idx].id for idx in range(start, end)]
195209
}
196-
resp = self.session.post(url, params=params)
197-
if resp.status_code != 200:
198-
raise APIError(resp.text)
210+
self.parse_response(self.session.post(url, params=params), json_data=False)
199211

200212
def add_tag(self, articles, tag):
201-
self.add_general_label(articles, 'user/-/label/{}'.format(tag))
213+
self.add_general_label(articles, self.GENERAL_TAG_TEMPLATE.format(tag))
202214

203215
def mark_as_read(self, articles):
204-
self.add_general_label(articles, 'user/-/state/com.google/read')
216+
self.add_general_label(articles, self.READ_TAG)
205217

206218
def mark_as_starred(self, articles):
207-
self.add_general_label(articles, 'user/-/state/com.google/starred')
219+
self.add_general_label(articles, self.STARRED_TAG)
208220

209221
def mark_as_liked(self, articles):
210-
self.add_general_label(articles, 'user/-/state/com.google/like')
222+
self.add_general_label(articles, self.LIKED_TAG)
211223

212224
def broadcast(self, articles):
213-
self.add_general_label(articles, 'user/-/state/com.google/broadcast')
225+
self.add_general_label(articles, self.BROADCAST_TAG)

0 commit comments

Comments
 (0)