Skip to content
This repository has been archived by the owner on Aug 30, 2019. It is now read-only.

Commit

Permalink
Merge pull request #5 from pauloromeira/unlogged
Browse files Browse the repository at this point in the history
Add Unlogged session behavior
  • Loading branch information
pauloromeira authored May 6, 2018
2 parents 54b4363 + 5555b44 commit 0334649
Show file tree
Hide file tree
Showing 65 changed files with 4,242 additions and 3,082 deletions.
20 changes: 6 additions & 14 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
[ ] Activity (https://www.instagram.com/accounts/activity/?__a=1)
[ ] Suggested Users (https://www.instagram.com/explore/people/ | e3c04bbb35d86a16cf0e7881c5057737)
[ ] Saved Posts (https://www.instagram.com/<me>/saved/?__a=1)
[ ] Tag Posts (https://www.instagram.com/explore/tags/recife/?__a=1)
[ ] Explore Locations (https://www.instagram.com/explore/locations/?__a=1)
[ ] Profiles (https://www.instagram.com/directory/profiles/)
Other:
Expand All @@ -20,12 +19,6 @@
[ ] Make post queries accept id and url
[ ] Make user queries accept url
- ...
* Tests
- [ ] Session
- [X] Actions
- [ ] Queries
- [ ] Coverage
- ...
* Other:
[ ] Read settings from YAML
[ ] Infer rate limis
Expand All @@ -35,8 +28,6 @@
[ ] Exceptions
[ ] Logs
[ ] Algorithms (with session awareness)
[ ] Unlogged use (queries: posts, comments, post_info, user_info...)
[ ] Try unlogged first
[ ] Demo gif (README.md)
[ ] Post actions: translate shortcode -> id (post_ifo)
- ...
3 changes: 2 additions & 1 deletion onegram/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .session import Login
from .session import Login, Unlogged
from .session import login, logout
from .session import unlogged, close

from .actions import follow, unfollow
from .actions import like, unlike
Expand Down
4 changes: 4 additions & 0 deletions onegram/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class AuthFailed(AuthException):
class AuthUserError(AuthException):
pass

class NotSupportedError(OnegramException):
pass


# TODO [romeira]: Query/action exceptions {06/03/18 23:08}
# TODO [romeira]: Session expired exception {06/03/18 23:08}
# TODO [romeira]: Private user exception/warning {06/03/18 23:09}
Expand Down
9 changes: 9 additions & 0 deletions onegram/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from .utils import jsearch
from .constants import URLS, GRAPHQL_URL
from .constants import QUERY_HASHES, JSPATHS
from .exceptions import NotSupportedError


@sessionaware
def user_info(session, username=None):
if session.unlogged and not username:
raise NotSupportedError('You must provide an user at Unlogged state')
return _info(session, username=username or session.username)

@sessionaware
Expand All @@ -16,14 +19,20 @@ def post_info(session, post):

@sessionaware
def followers(session, user=None):
if session.unlogged and not user:
raise NotSupportedError('You must provide an user at Unlogged state')
yield from _iter_user(session, user)

@sessionaware
def following(session, user=None):
if session.unlogged and not user:
raise NotSupportedError('You must provide an user at Unlogged state')
yield from _iter_user(session, user)

@sessionaware
def posts(session, user=None):
if session.unlogged and not user:
raise NotSupportedError('You must provide an user at Unlogged state')
yield from _iter_user(session, user)

@sessionaware
Expand Down
141 changes: 95 additions & 46 deletions onegram/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,70 +18,69 @@
from .constants import DEFAULT_HEADERS, QUERY_HEADERS, ACTION_HEADERS
from .constants import URLS
from .constants import REGEXES
from .exceptions import AuthException
from .exceptions import AuthException, NotSupportedError
from .utils.ratelimit import RateLimiter
from .utils.validation import check_auth


class Login(Session):
class _BaseSession(Session):


@classmethod
def current(cls):
return Session.current() or login()

@property
def current_module_name(self):
return (self.current_function.__module__.split('.', 1)[-1]
if self.current_function else None)

@property
def current_function_name(self):
return (self.current_function.__name__
if self.current_function else None)

@property
def current_module_name(self):
return (self.current_function.__module__.split('.', 1)[-1]
if self.current_function else None)

@property
def cookies(self):
return self._requests.cookies


def __init__(self, username=None, password=None, custom_settings={}):
if username:
custom_settings['USERNAME'] = username
if password:
custom_settings['PASSWORD'] = password
@property
def unlogged(self):
return isinstance(self, Unlogged)


def __init__(self, custom_settings={}):
self.settings = _load_settings(custom_settings)

log_settings = self.settings.get('LOG_SETTINGS')
if log_settings:
logging.basicConfig(**log_settings)

self.username = self.settings.get('USERNAME')


def enter_contexts(self):
self._requests = yield requests.Session()

# Security config
proxies = self.settings.get('PROXIES')
if proxies:
self._requests.proxies = proxies

verify_ssl = self.settings.get('VERIFY_SSL', True)
self._requests.verify = verify_ssl
if not verify_ssl:
urllib3.disable_warnings(InsecureRequestWarning)

# Init headers
self._requests.headers.update(DEFAULT_HEADERS)
user_agent = self.settings.get('USER_AGENT')
if user_agent is not None:
self._requests.headers['User-Agent'] = user_agent
else:
self._requests.headers.pop('User-Agent', None)

try:
self._login()
except AuthException as e:
self.logger.error(e)
self.close()
raise e
response = self._requests.get(URLS['start'])
response.raise_for_status()
self.rhx_gis = self._get_rhx_gis(response)

self.rate_limiter = RateLimiter(self)

Expand Down Expand Up @@ -121,12 +120,56 @@ def _request():
return _request()


def _login(self):
start_url, login_url = URLS['start'], URLS['login']
response = self._requests.get(start_url)
response.raise_for_status()
self.rhx_gis = self._get_rhx_gis(response)
def _get_rhx_gis(self, response):
match = re.search(REGEXES['rhx_gis'], response.text)
return match.group(1) if match else None


def _build_signature(self, url, params):
if self.current_function_name in ('post_info', 'user_info'):
var = parse_url(url).path
else:
var = params.get('variables')

payload = f'{self.rhx_gis}:{var}'
return hashlib.md5(payload.encode('utf-8')).hexdigest()


@property
def logger(self):
name = f'{__name__}:{self}'
if self.current_function:
name += f' {self.current_module_name}.{self.current_function_name}'
return logging.getLogger(name)


class Login(_BaseSession):


def __init__(self, username=None, password=None, custom_settings={}):
if username:
custom_settings['USERNAME'] = username
if password:
custom_settings['PASSWORD'] = password

super(Login, self).__init__(custom_settings)

self.username = self.settings.get('USERNAME')
# TODO [romeira]: fix sessionlib {06/05/18 04:41}
# self.on_open.subscribe(self._login)


def enter_contexts(self):
yield from super(Login, self).enter_contexts()
try:
self._login()
except AuthException as e:
self.logger.error(e)
self.close()
raise e


def _login(self):
kw = {}
self.username = self.username or input('Username: ')
kw['data'] = {
Expand All @@ -139,51 +182,57 @@ def _login(self):
headers['X-CSRFToken'] = self.cookies['csrftoken']
kw['headers'] = headers

response = self._requests.post(login_url, **kw)
response = self._requests.post(URLS['login'], **kw)
response.raise_for_status()
check_auth(json.loads(response.text))
self.user_id = self.cookies.get('ds_user_id')


def _get_rhx_gis(self, response):
match = re.search(REGEXES['rhx_gis'], response.text)
return match.group(1) if match else None
def __str__(self):
return f'({self.username})'


def _build_signature(self, url, params):
if self.current_function_name in ('post_info', 'user_info'):
var = parse_url(url).path
else:
var = params.get('variables')
class Unlogged(_BaseSession):

payload = f'{self.rhx_gis}:{var}'
return hashlib.md5(payload.encode('utf-8')).hexdigest()
supported = ['user_info', 'post_info', 'posts', 'comments', 'explore_tag']

def __init__(self, custom_settings={}):
super(Unlogged, self).__init__(custom_settings)

@property
def logger(self):
name = f'{__name__}:{self}'
if self.current_function:
name += f' {self.current_module_name}.{self.current_function_name}'
return logging.getLogger(name)

def request(self, *a, **kw):
fn = self.current_function_name
if fn not in Unlogged.supported:
msg = f'"{fn}" is not supported at Unlogged state'
raise NotSupportedError(msg)

return super(Unlogged, self).request(*a, **kw)


def __str__(self):
return f'({self.username})'
str = super(Unlogged, self).__str__()
return f'(Unlogged: {str})'


sessionaware = _sessionaware(cls=Login)
sessionaware = _sessionaware(cls=_BaseSession)


def login(*args, **kwargs):
return Login(*args, **kwargs).open()


@_sessionaware
def logout(session):
def close(session):
session.close()


logout = close


def unlogged(*args, **kwargs):
return Unlogged(*args, **kwargs).open()


def _load_settings(custom_settings={}):
settings = {k:getattr(settings_module, k)
for k in dir(settings_module) if k.isupper()}
Expand Down
9 changes: 7 additions & 2 deletions onegram/utils/ratelimit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ def __init__(self, session):
if self.persist_enabled:
persist_dir = session.settings.get('RATE_PERSIST_DIR',
Path('.onegram/rates'))
self.persist_path = (Path(persist_dir) /
f'{self.session.username}.json')

if session.unlogged:
filename = '~unlogged.json'
else:
filename = f'{self.session.username}.json'

self.persist_path = Path(persist_dir) / filename
self.load()


Expand Down
Loading

0 comments on commit 0334649

Please sign in to comment.