1
0
mirror of https://github.com/gryf/slack-backup.git synced 2025-12-17 11:30:25 +01:00

Added reporters module

This commit is contained in:
2016-11-25 07:45:37 +01:00
parent f3bafadbfe
commit 97069e0a83
3 changed files with 136 additions and 34 deletions

View File

@@ -4,12 +4,14 @@ Create backup for certain date for specified channel in slack
from datetime import datetime from datetime import datetime
import getpass import getpass
import logging import logging
import os
import slackclient import slackclient
from slack_backup import db from slack_backup import db
from slack_backup import objects as o from slack_backup import objects as o
from slack_backup import download from slack_backup import download
from slack_backup import reporters
class Client(object): class Client(object):
@@ -18,23 +20,28 @@ class Client(object):
querying data fetched out using Slack API. querying data fetched out using Slack API.
""" """
def __init__(self, args): def __init__(self, args):
self.slack = slackclient.SlackClient(args.token) if 'token' in args:
self.slack = slackclient.SlackClient(args.token)
self.user = args.user
self.password = args.password
if not self.user and not self.password:
logging.warning('No media will be downloaded, due to not '
'providing credentials for a slack account')
elif not self.user and self.password:
logging.warning('No media will be downloaded, due to not '
'providing username for a slack account')
elif self.user and not self.password:
self.password = getpass.getpass(prompt='Provide password for '
'your slack account: ')
dbpath = self._get_asset_dir(args.database)
self.downloader = download.Download(args, dbpath)
self.engine = db.connect(args.database) self.engine = db.connect(args.database)
self.session = db.Session() self.session = db.Session()
self.selected_channels = args.channels self.selected_channels = args.channels
self.user = args.user
self.password = args.password
if not self.user and not self.password:
logging.warning('No media will be downloaded, due to not '
'providing credentials for a slack account')
elif not self.user and self.password:
logging.warning('No media will be downloaded, due to not '
'providing username for a slack account')
elif self.user and not self.password:
self.password = getpass.getpass(prompt='Provide password for '
'your slack account: ')
self.q = self.session.query self.q = self.session.query
self.downloader = download.Download(args)
if 'format' in args:
self.reporter = reporters.get_reporter(args, self.q)
def update(self): def update(self):
""" """
@@ -117,6 +124,7 @@ class Client(object):
latest = latest and latest.ts or 1 latest = latest and latest.ts or 1
while True: while True:
logging.debug("Fetching another portion of messages")
messages, latest = self._channels_history(channel, latest) messages, latest = self._channels_history(channel, latest)
for msg in messages: for msg in messages:
@@ -131,13 +139,13 @@ class Client(object):
""" """
Return a history accumulated in DB into desired format. Special format Return a history accumulated in DB into desired format. Special format
""" """
self.reporter.generate()
def _create_message(self, data, channel): def _create_message(self, data, channel):
""" """
Create message with corresponding possible metadata, like reactions, Create message with corresponding possible metadata, like reactions,
files etc. files etc.
""" """
logging.info("Fetching messages for channel %s", channel.name)
message = o.Message(data) message = o.Message(data)
message.user = self.q(o.User).\ message.user = self.q(o.User).\
filter(o.User.slackid == data['user']).one() filter(o.User.slackid == data['user']).one()
@@ -173,8 +181,11 @@ class Client(object):
message.is_starred = True message.is_starred = True
if is_external: if is_external:
logging.debug("Found external file `%s'", data['url_private'])
message.file.url = data['url_private'] message.file.url = data['url_private']
else: else:
logging.debug("Found internal file `%s'",
data['url_private_download'])
priv_url = data['url_private_download'] priv_url = data['url_private_download']
message.file.filepath = self.downloader.download(priv_url, 'file') message.file.filepath = self.downloader.download(priv_url, 'file')
@@ -228,6 +239,17 @@ class Client(object):
channel.topic = self._get_create_obj(data['topic'], o.Topic, channel) channel.topic = self._get_create_obj(data['topic'], o.Topic, channel)
self.session.flush() self.session.flush()
def _get_asset_dir(self, database):
"""
Get absolute assets directory using sqlite database path as a
reference.
"""
if not database:
return 'assets'
path = os.path.dirname(os.path.abspath(database))
return os.path.join(path, 'assets')
def _channels_list(self): def _channels_list(self):
""" """
Get channel list using Slack API. Return list of channel data or None Get channel list using Slack API. Return list of channel data or None

View File

@@ -4,10 +4,11 @@ to local ones, so that sophisticated writers can make a use of it
""" """
import logging import logging
import os import os
import errno
import requests import requests
from slack_backup import utils
class NotAuthorizedError(requests.HTTPError): class NotAuthorizedError(requests.HTTPError):
pass pass
@@ -16,15 +17,15 @@ class NotAuthorizedError(requests.HTTPError):
class Download(object): class Download(object):
"""Download class for taking care of Slack internally uploaded files""" """Download class for taking care of Slack internally uploaded files"""
def __init__(self, args): def __init__(self, args, assets_dir):
self.session = requests.session() self.session = requests.session()
self.team = args.team self.team = args.team
self.user = args.user self.user = args.user
self.password = args.password self.password = args.password
self.assets_dir = args.assets self.assets_dir = assets_dir
self._files = os.path.join(self.assets_dir, 'files') self._files = os.path.join(self.assets_dir, 'files')
self._images = os.path.join(self.assets_dir, 'images') self._images = os.path.join(self.assets_dir, 'images')
self._do_download = False self._authorized = False
self._hier_created = False self._hier_created = False
self.cookies = {} self.cookies = {}
@@ -32,8 +33,6 @@ class Download(object):
""" """
Download asset, return local path to it Download asset, return local path to it
""" """
if not self._do_download:
return
if not self._hier_created: if not self._hier_created:
self._create_assets_dir() self._create_assets_dir()
@@ -44,12 +43,8 @@ class Download(object):
return fname return fname
def _create_assets_dir(self): def _create_assets_dir(self):
for name in ('images', 'files'): for path in (self._files, self._images):
try: utils.makedirs(path)
os.makedirs(os.path.join(self.assets_dir, name))
except OSError as err:
if err.errno != errno.EEXIST:
raise
self._hier_created = True self._hier_created = True
@@ -59,6 +54,11 @@ class Download(object):
typemap = {'avatar': self._images, typemap = {'avatar': self._images,
'file': self._files} 'file': self._files}
if filetype == 'file' and not self._authorized:
logging.info("There was no (valid) credentials passed, therefore "
"file `%s' cannot be downloaded", url)
return
splitted = url.split('/') splitted = url.split('/')
if len(splitted) == 7 and 'slack.com' in splitted[2]: if len(splitted) == 7 and 'slack.com' in splitted[2]:
@@ -72,12 +72,8 @@ class Download(object):
path = typemap[filetype] path = typemap[filetype]
if part: if part:
try: utils.makedirs(os.path.join(path, part))
path = os.path.join(path, part) path = os.path.join(path, part)
os.makedirs(path)
except OSError as err:
if err.errno != errno.EEXIST:
raise
path = os.path.join(path, fname) path = os.path.join(path, fname)
count = 1 count = 1
@@ -91,11 +87,18 @@ class Download(object):
def _download(self, url, local): def _download(self, url, local):
"""Download file""" """Download file"""
res = self.session.get(url, stream=True) try:
res = self.session.get(url, stream=True)
except requests.exceptions.RequestException as exc:
logging.error('Request for %s failed. Reported reason: %s',
url, exc.__doc__)
raise
with open(local, 'wb') as fobj: with open(local, 'wb') as fobj:
for chunk in res.iter_content(chunk_size=5120): for chunk in res.iter_content(chunk_size=5120):
if chunk: if chunk:
fobj.write(chunk) fobj.write(chunk)
logging.debug("Downloaded `%s' to `'%s'", url, local)
def authorize(self): def authorize(self):
""" """
@@ -126,4 +129,4 @@ class Download(object):
('a-' + self.cookies['a']) in self.cookies): ('a-' + self.cookies['a']) in self.cookies):
logging.error('Failed to login into Slack app') logging.error('Failed to login into Slack app')
else: else:
self._do_download = True self._authorized = True

77
slack_backup/reporters.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Reporters module.
There are several classes for specific format reporting, and also some of the
slack conversation/convention parsers.
"""
import os
import logging
from slack_backup import objects as o
class Reporter(object):
"""Base reporter class"""
ext = ''
def __init__(self, args, query):
self.out = args.output
self.q = query
self.channels = self._get_channels(args.channels)
def _get_channels(self, selected_channels):
"""
Retrive channels from db and return those which names matched from
selected_channels list
"""
result = []
all_channels = self.q(o.Channel).all()
if not selected_channels:
return all_channels
for channel in all_channels:
if channel.name in selected_channels:
result.append(channel)
return channel
def generate(self):
"""Generate raport it's a dummmy one - for use with none reporter"""
return
def get_log_path(self, name):
"""Return relative log file name """
return os.path.join(self.out, name + self.ext)
def write_msg(self, message, log):
"""Write message to file"""
raise NotImplementedError()
class TextReporter(Reporter):
"""Text aka IRC reporter"""
ext = '.log'
def generate(self):
"""Generate raport"""
for channel in self.channels:
log_path = self.get_log_path(channel.name)
for message in self.q(o.Message).\
filter(o.Message.channel == channel).\
order_by(o.Message.ts).all():
self.write_msg(message, log_path)
def write_msg(self, message, log):
"""Write message to file"""
def get_reporter(args, query):
"""Return object of right reporter class"""
reporters = {'text': TextReporter}
klass = reporters.get(args.format, Reporter)
if klass.__name__ == 'Reporter':
logging.warning('None, or wrong (%s) formatter selected, falling to'
' None Reporter', args.format)
return klass(args, query)