New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

stackl

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

stackl - pypi Package Compare versions

Comparing version
0.0.0a0
to
0.0.1
+210
stackl/__init__.py
import sys
import threading
from logging import StreamHandler
import logging
import os.path
import pickle
import json
import requests
from bs4 import BeautifulSoup
from stackl.errors import LoginError, InvalidOperationError
from stackl.models import Room
from stackl.events import Event
from stackl.wsclient import WSClient
VERSION = '0.0.1'
class ChatClient:
def __init__(self, **kwargs):
"""
Initialise a new ChatClient object. Valid kwargs are:
:param kwargs['default_server']: one of stackexchange.com, stackoverflow.com, or meta.stackexchange.com,
depending on where you want the client to default to.
:param kwargs['log_location']: a logging.Handler object (e.g. StreamHandler or FileHandler) specifying a log
location
:param kwargs['log_level']: an integer, usually one of the logging.* constants such as logging.DEBUG, specifying
the minimum effective log level
"""
self.default_server = kwargs.get('default_server') or 'stackexchange.com'
log_location = kwargs.get('log_location') or StreamHandler(stream=sys.stdout)
log_level = kwargs.get('log_level') or logging.DEBUG
self.logger = logging.getLogger('stackl')
self.logger.setLevel(log_level)
self.logger.addHandler(log_location)
self.session = requests.Session()
self.session.headers.update({'User-Agent': 'stackl'})
self.rooms = []
self._handlers = []
self._sockets = {}
self._fkeys = {}
self._authed_servers = []
def login(self, email, password, **kwargs):
"""
Log the client instance into Stack Exchange. Will default to logging in using cached cookies, if provided, and
fall back to logging in with credentials.
:param email: the email of the Stack Exchange account you want to log in as
:param password: the corresponding account password
:param kwargs: pass "cookie_file" to specify where cached cookies are located (must be a pickle file)
:return: the logged-in requests.Session if successful
"""
logged_in = False
if 'cookie_file' in kwargs and os.path.exists(kwargs['cookie_file']):
with open(kwargs['cookie_file'], 'rb') as f:
self.session.cookies.update(pickle.load(f))
logged_in = self._verify_login(kwargs.get('servers') or [self.default_server])
if logged_in is False:
self.logger.warn('Cookie login failed. Falling back to credential login.')
for n, v in self.session.cookies.items():
self.logger.info('{}: {}'.format(n, v))
if not logged_in:
logged_in = self._credential_authenticate(email, password, kwargs.get('servers') or [self.default_server])
if not logged_in:
self.logger.critical('All login methods failed. Cannot log in to SE.')
raise LoginError('All available login methods failed.')
else:
self._authed_servers = kwargs.get('servers') or [self.default_server]
return self.session
def join(self, room_id, server):
"""
Join a room and start processing events from it.
:param room_id: the ID of the room you wish to join
:param server: the server on which the room is hosted
:return: None
"""
if server not in self._authed_servers:
raise InvalidOperationError('Cannot join a room on a host we haven\'t authenticated to!')
room = Room(server, room_id=room_id)
self.rooms.append(room)
self.session.get("https://chat.{}/rooms/{}".format(server, room_id), data={'fkey': self._fkeys[server]})
events = self.session.post("https://chat.{}/chats/{}/events".format(server, room_id), data={
'fkey': self._fkeys[server],
'since': 0,
'mode': 'Messages',
'msgCount': 100
}).json()['events']
event_data = [Event(x, server) for x in events]
room.add_events(event_data)
ws_auth_data = self.session.post("https://chat.{}/ws-auth".format(server), data={
'fkey': self._fkeys[server],
'roomid': room_id
}).json()
cookie_string = ''
for cookie in self.session.cookies:
if cookie.domain == 'chat.{}'.format(server) or cookie.domain == '.{}'.format(server):
cookie_string += '{}={};'.format(cookie.name, cookie.value)
last_event_time = sorted(events, key=lambda x: x['time_stamp'])[-1]['time_stamp']
ws_uri = '{}?l={}'.format(ws_auth_data['url'], last_event_time)
if server in self._sockets and self._sockets[server].open:
self._sockets[server].close()
self._sockets[server] = WSClient(ws_uri, cookie_string, server, self._on_message)
def send(self, content, room=None, server=None):
"""
Send a message to the specified room.
:param content: the contents of the message you wish to send
:param room: the ID of the room you wish to send it to
:param server: the server on which the room is hosted
:return: None
"""
if room is None or server is None:
raise InvalidOperationError('Cannot send a message to a non-existent room or a non-existent server.')
# TODO
def add_handler(self, handler, **kwargs):
"""
Add an event handler for messages received from the chat websocket.
:param handler: the handler method to call for each received event
:return: None
"""
self._handlers.append([handler, kwargs])
def _credential_authenticate(self, email, password, servers):
"""
Authenticate with Stack Exchange using provided credentials.
:param email: the email of the Stack Exchange account you want to log in as
:param password: the corresponding account password
:return: a success boolean
"""
fkey_page = self.session.get("https://stackapps.com/users/login")
fkey_soup = BeautifulSoup(fkey_page.text, 'html.parser')
fkey_input = fkey_soup.select('input[name="fkey"]')
if len(fkey_input) <= 0:
raise LoginError('Failed to get fkey from StackApps. Wat?')
fkey = fkey_input[0].get('value')
login_post = self.session.post("https://stackapps.com/users/login", data={
'email': email,
'password': password,
'fkey': fkey
})
login_soup = BeautifulSoup(login_post.text, 'html.parser')
iframes = login_soup.find_all('iframe')
if any(['captcha' in x.get('src') for x in iframes]):
raise LoginError('Login triggered a CAPTCHA - cannot proceed.')
tokens = self.session.post("https://stackapps.com/users/login/universal/request", headers={
'Referer': 'https://stackapps.com/'
}).json()
for site_token in tokens:
self.session.get("https://{}/users/login/universal.gif".format(site_token['Host']), data={
'authToken': site_token['Token'],
'nonce': site_token['Nonce']
}, headers={
'Referer': 'https://stackapps.com/'
})
return self._verify_login(servers)
def _verify_login(self, servers):
"""
Verifies that login with cached cookies has been successful for all the given chat servers.
:param servers: a list of servers to check for successful logins
:return: a success boolean
"""
statuses = []
for server in servers:
chat_home = self.session.get("https://chat.{}/".format(server))
chat_soup = BeautifulSoup(chat_home.text, 'html.parser')
self._fkeys[server] = chat_soup.select('input[name="fkey"]')[0].get('value')
topbar_links = chat_soup.select('.topbar-links span.topbar-menu-links a')
if len(topbar_links) <= 0:
raise LoginError('Unable to verify login because page layout wasn\'t as expected. Wat?')
elif topbar_links[0].text == 'log in':
raise LoginError('Failed to log in to {}'.format(server))
else:
statuses.append(True)
return len(statuses) == 3 and all(statuses)
def _on_message(self, data, server):
data = json.loads(data)
events = [v['e'] for k, v in data.items() if k[0] == 'r' and 'e' in v]
events = [x for s in events for x in s]
for event_data in events:
event = Event(event_data, server)
handlers = [x[0] for x in self._handlers
if all([k in event_data and event_data[k] == v for k, v in x[1].items()])]
for handler in handlers:
def run_handler():
handler(event, server)
threading.Thread(name='handler_runner', target=run_handler).start()
class LoginError(BaseException):
pass
class InvalidOperationError(BaseException):
pass
from collections import namedtuple
from stackl.models import *
EventClassData = namedtuple('EventClassData', ['id', 'classes'])
EventClass = namedtuple('EventClass', ['type', 'fields', 'target_prop'])
EventField = namedtuple('EventField', ['target_prop', 'source_prop'])
EVENT_SHORTHAND = ['message', 'edit', 'entrance', 'exit', 'rename', 'star', 'debug', 'mention', 'flag', 'delete',
'file', 'mod-flag', 'settings', 'gnotif', 'level', 'lnotif', 'invite', 'reply', 'move-out',
'move-in', 'time', 'feed', 'suspended', 'merge']
EVENT_NAME = ['Message Posted', 'Message Edited', 'User Entered', 'User Left', 'Room Name Changed', 'Message Starred',
'Debug Message', 'User Mentioned', 'Message Flagged', 'Message Deleted', 'File Added', 'Moderator Flag',
'User Settings Changed', 'Global Notification', 'Access Level Changed', 'User Notification',
'Invitation', 'Message Reply', 'Message Moved Out', 'Message Moved In', 'Time Break', 'Feed Ticker',
'User Suspended', 'User Merged']
EVENT_CLASSES = [
EventClassData(1, [
EventClass(Message, [
EventField('timestamp', 'time_stamp'),
EventField('content', None),
EventField('room_id', None),
EventField('user_id', None),
EventField('message_id', None),
EventField('parent_id', None)
], None)
]),
EventClassData(2, [
EventClass(Message, [
EventField('timestamp', 'time_stamp'),
EventField('content', None),
EventField('room_id', None),
EventField('user_id', None),
EventField('message_id', None)
], None)
]),
EventClassData(3, [
EventClass(User, [
EventField('user_id', 'target_user_id')
], None),
EventClass(Room, [
EventField('room_id', None)
], None)
]),
EventClassData(4, [
EventClass(User, [
EventField('user_id', 'target_user_id')
], None),
EventClass(Room, [
EventField('room_id', None)
], None)
]),
EventClassData(5, [
EventClass(Room, [
EventField('room_id', None)
], None)
]),
EventClassData(8, [
EventClass(Message, [
EventField('timestamp', 'time_stamp'),
EventField('content', None),
EventField('room_id', None),
EventField('user_id', None),
EventField('message_id', None)
], None),
EventClass(User, [
EventField('user_id', None)
], 'source_user'),
EventClass(User, [
EventField('user_id', 'target_user_id')
], 'target_user')
]),
EventClassData(18, [
EventClass(Message, [
EventField('timestamp', 'time_stamp'),
EventField('content', None),
EventField('room_id', None),
EventField('user_id', None),
EventField('message_id', None)
], None)
])
]
class Event:
def __init__(self, event_dict, server):
self.type_id = int(event_dict['event_type'])
self.name = EVENT_NAME[self.type_id - 1]
self.shorthand = EVENT_SHORTHAND[self.type_id - 1]
self.raw = event_dict
self.server = server
class_data = [x for x in EVENT_CLASSES if x.id == self.type_id]
if len(class_data) > 0:
class_data = class_data[0]
for type in class_data.classes:
clazz = type.type
if type.target_prop is not None:
method_name = type.target_prop
else:
method_name = type.type.__name__.lower()
initialization_props = {}
for field in type.fields:
if field.source_prop is not None:
if field.source_prop not in event_dict:
continue
initialization_props[field.target_prop] = event_dict[field.source_prop]
else:
if field.target_prop not in event_dict:
continue
initialization_props[field.target_prop] = event_dict[field.target_prop]
type_object = clazz(server, **initialization_props)
setattr(self, method_name, type_object)
for k, v in event_dict.items():
setattr(self, k, v)
def __repr__(self):
return '<Event {}>'.format(self.__dict__)
class Helpers:
_cache = {}
@classmethod
def cached(cls, key, scope=None, func=None):
if scope is not None:
if scope not in cls._cache:
cls._cache[scope] = {}
if key in cls._cache[scope]:
return cls._cache[scope][key]
else:
result = None if func is None else func()
cls._cache[scope][key] = result
return result
else:
if key in cls._cache:
return cls._cache[key]
else:
result = None if func is None else func()
cls._cache[key] = result
return result
@classmethod
def cache(cls, key, scope=None, object=None):
if scope is not None:
if scope not in cls._cache:
cls._cache[scope] = {}
cls._cache[scope][key] = object
else:
cls._cache[key] = object
import re
import requests
from bs4 import BeautifulSoup
from stackl.helpers import Helpers
from stackl.tasks import Tasks
class Room:
def __init__(self, server, **kwargs):
self.id = int(kwargs.get('room_id'))
self.server = server
self.url = "https://chat.{}/rooms/{}".format(server, kwargs.get('room_id'))
self.owners = []
self.events = []
Tasks.do(self._scrape_room_info)
def _scrape_room_info(self):
info_page = requests.get("https://chat.{}/rooms/info/{}".format(self.server, self.id))
room_soup = BeautifulSoup(info_page.text, 'html.parser')
metadata_card = room_soup.select('.roomcard-xxl')[0]
self.name = metadata_card.find('h1').text
self.description = metadata_card.find('p').text
owner_cards = room_soup.select('.room-ownercards .usercard')
for card in owner_cards:
user_id = card.get('id').split('-')[-1]
self.owners.append(Helpers.cached(int(user_id), 'users', lambda: User(self.server, user_id=user_id)))
Helpers.cache(self.id, 'rooms', self)
def add_events(self, events):
self.events.extend(events)
class User:
def __init__(self, server, **kwargs):
self.id = int(kwargs.get('user_id'))
self.server = server
self.url = "https://chat.{}/users/{}".format(server, kwargs.get('user_id'))
self.in_rooms = []
self.owns_rooms = []
Tasks.do(self._scrape_user_info)
def _scrape_user_info(self):
user_page = requests.get(self.url)
user_soup = BeautifulSoup(user_page.text, 'html.parser')
self.username = user_soup.select('.usercard-xxl .user-status')[0].text
self.bio = user_soup.select('.user-stats tr')[3].select('td')[-1].text
in_room_cards = user_soup.select('#user-roomcards-container .roomcard')
self.in_rooms.extend(self._initialize_rooms(in_room_cards))
owns_room_cards = user_soup.select('#user-owningcards .roomcard')
self.owns_rooms.extend(self._initialize_rooms(owns_room_cards))
Helpers.cache(self.id, 'users', self)
def _initialize_rooms(self, card_list):
for room_card in card_list:
room_id = room_card.get('id').split('-')[-1]
yield Helpers.cached(int(room_id), 'rooms', lambda: Room(self.server, room_id=room_id))
class Message:
def __init__(self, server, **kwargs):
self.server = server
self.id = int(kwargs.get('message_id'))
self.timestamp = kwargs.get('timestamp')
self.content = kwargs.get('content')
self.room = Helpers.cached(int(kwargs.get('room_id')), 'rooms',
lambda: Room(server, room_id=kwargs.get('room_id')))
self.user = Helpers.cached(int(kwargs.get('user_id')), 'users',
lambda: User(server, user_id=kwargs.get('user_id')))
self.parent_id = kwargs.get('parent_id')
self._setup_delegate_methods()
def reply(self, client, content):
client.send(':{} {}'.format(self.id, content), room=self.room, server=self.server)
def is_reply(self):
return re.match(r'^:\d+ ', self.content) is not None
# Less ugly than having a method for every one of these that does exactly the same thing.
def _setup_delegate_methods(self):
method_names = ['toggle_star', 'star_count', 'star', 'unstar', 'has_starred', 'cancel_stars', 'delete', 'edit',
'toggle_pin', 'pin', 'unpin', 'is_pinned']
def create_delegate(method_name):
def delegate(client):
getattr(client, method_name)(self.id, self.server)
return delegate
for name in method_names:
setattr(self, name, create_delegate(name))
import asyncio
import threading
class Tasks:
loop = asyncio.new_event_loop()
@classmethod
def _run(cls):
asyncio.set_event_loop(cls.loop)
try:
cls.loop.run_forever()
finally:
cls.loop.close()
@classmethod
def do(cls, func, *args, **kwargs):
handle = cls.loop.call_soon(lambda: func(*args, **kwargs))
cls.loop._write_to_self()
return handle
@classmethod
def later(cls, func, *args, after=None, **kwargs):
handle = cls.loop.call_later(after, lambda: func(*args, **kwargs))
cls.loop._write_to_self()
return handle
@classmethod
def periodic(cls, func, *args, interval=None, **kwargs):
@asyncio.coroutine
def f():
while True:
yield from asyncio.sleep(interval)
func(*args, **kwargs)
handle = cls.loop.create_task(f())
cls.loop._write_to_self()
return handle
threading.Thread(name="tasks", target=Tasks._run, daemon=True).start()
import threading
import websocket as ws
import requests
class WSClient:
def __init__(self, url, cookies, server, handler):
self.url = url
self.server = server
self.cookies = cookies
self.handler = handler
self.open = False
self._close_socket = False
self.ws = None
threading.Thread(name='wsclient', target=self._run_websocket).start()
def _run_websocket(self):
self.ws = ws.create_connection(self.url, origin='https://chat.{}'.format(self.server), cookie=self.cookies)
self.open = True
while not self._close_socket:
try:
data = self.ws.recv()
except (ws.WebSocketConnectionClosedException, requests.ConnectionError):
self.open = False
threading.Thread(name='wsclient', target=self._run_websocket).start()
break
self.handler(data, self.server)
if self.ws.connected:
self.ws.close()
def close(self):
self._close_socket = True
if self.ws is not None:
self.ws.close()
+2
-2
Metadata-Version: 2.1
Name: stackl
Version: 0.0.0a0
Version: 0.0.1
Summary: Python library for connecting to Stack Exchange chat
Home-page: https://github.com/ArtOfCode-/stat
Home-page: https://github.com/ArtOfCode-/stackl
Author: ArtOfCode

@@ -7,0 +7,0 @@ Author-email: hello@artofcode.co.uk

import setuptools
import stackl

@@ -8,3 +9,3 @@ with open("README.md", "r") as fh:

name="stackl",
version="0.0.0a",
version=stackl.VERSION,
author="ArtOfCode",

@@ -15,3 +16,3 @@ author_email="hello@artofcode.co.uk",

long_description_content_type="text/markdown",
url="https://github.com/ArtOfCode-/stat",
url="https://github.com/ArtOfCode-/stackl",
packages=setuptools.find_packages(),

@@ -18,0 +19,0 @@ classifiers=[

Metadata-Version: 2.1
Name: stackl
Version: 0.0.0a0
Version: 0.0.1
Summary: Python library for connecting to Stack Exchange chat
Home-page: https://github.com/ArtOfCode-/stat
Home-page: https://github.com/ArtOfCode-/stackl
Author: ArtOfCode

@@ -7,0 +7,0 @@ Author-email: hello@artofcode.co.uk

README.md
setup.py
stackl/__init__.py
stackl/errors.py
stackl/events.py
stackl/helpers.py
stackl/models.py
stackl/tasks.py
stackl/wsclient.py
stackl.egg-info/PKG-INFO
stackl.egg-info/SOURCES.txt
stackl.egg-info/dependency_links.txt
stackl.egg-info/top_level.txt
stat/__init__.py
stackl.egg-info/top_level.txt

@@ -1,1 +0,1 @@

stat
stackl
name = "stackl"