1
0
mirror of https://github.com/gryf/slack-backup.git synced 2025-12-17 19:40:21 +01:00

12 Commits
v0.2 ... v0.4.4

Author SHA1 Message Date
9ddd470b54 Move commands functions to its own module 2016-11-28 19:05:26 +01:00
feb773956c Fix for extension of config file 2016-11-28 18:25:41 +01:00
3f95986981 Fix the tests.
For some reason, database key was treated differently in configparser object
in python 3.4.2. In Python 3.4.5 everything is fine. Fixed the defaults to
make sure all string options are treated equally.
2016-11-28 18:14:47 +01:00
6d5f3746a2 Superfast fix for non-existed config parameter in cmdline options 2016-11-28 17:20:14 +01:00
7ccc2bddaa Added section about config in readme 2016-11-28 16:54:48 +01:00
db658f917f Added manifest 2016-11-27 20:52:34 +01:00
8568b552ca Readme update 2016-11-27 20:39:47 +01:00
c1c4581248 Added config file 2016-11-27 20:39:20 +01:00
b5e9c150ed Merge to head 2016-11-26 19:23:03 +01:00
af7f24e9a9 Dropping Python 2 support 2016-11-26 19:13:12 +01:00
11241e9d8b Readme update 2016-11-26 18:40:54 +01:00
c8c1dd4bfe Small fixes, version bump, alpha stage, added emoji map. 2016-11-26 18:03:29 +01:00
14 changed files with 437 additions and 150 deletions

View File

@@ -1,8 +1,6 @@
language: python language: python
env: env:
- TOXENV=py27
- TOXENV=py34 - TOXENV=py34
- TOXENV=py27-flake8
- TOXENV=py34-flake8 - TOXENV=py34-flake8
install: pip install tox install: pip install tox
script: tox script: tox

2
MANIFEST.in Normal file
View File

@@ -0,0 +1,2 @@
include README.rst
include config_example

View File

@@ -4,15 +4,18 @@ Slack backup
.. image:: https://travis-ci.org/gryf/slack-backup.svg?branch=master .. image:: https://travis-ci.org/gryf/slack-backup.svg?branch=master
:target: https://travis-ci.org/gryf/slack-backup :target: https://travis-ci.org/gryf/slack-backup
This project aim is to collect conversations from Slack using its API and .. image:: https://img.shields.io/pypi/v/slack-backup.svg
:target: https://pypi.python.org/pypi/slack-backup
The project aim is to collect conversations from Slack using its API and
optionally user account information, and provides convenient way to represent optionally user account information, and provides convenient way to represent
as a log. as a log.
Requirements Requirements
------------ ------------
This project is written in Python 2.7, and 3.4+, although version 2.7, which This project is written in Python 3, 3.4 to be precise, although it may work on
should work, wasn't tested as extensively as it should be. earlier version of Python3. Sorry no support for Python2.
Other than that, required packages are as follows: Other than that, required packages are as follows:
@@ -65,7 +68,7 @@ typical session:
(myenv)user@localhost ~/mylogs $ slack-backup fetch \ (myenv)user@localhost ~/mylogs $ slack-backup fetch \
--token xxxx-1111111111-222222222222-333333333333-r4nd0ms7uff \ --token xxxx-1111111111-222222222222-333333333333-r4nd0ms7uff \
--user some@email.address.org --password secret --team myteam \ --user some@email.address.org --password secret --team myteam \
-qqq -d mydatabase.sqlite -qq -d mydatabase.sqlite
where: where:
@@ -113,6 +116,60 @@ command.
See help for the ``slack-backup`` command for complete list of options. See help for the ``slack-backup`` command for complete list of options.
Configuration
-------------
For convenience, you can place all of needed options into configuration file
(aka .ini), which all options (with their defaults) will look like:
.. code:: ini
[common]
channels =
database =
quiet = 0
verbose = 0
[generate]
output =
format = text
theme = plain
[fetch]
user =
password =
team =
token =
Note, that you don't have to put every option. To illustrate ``fetch`` example
from above, here is a corresponding config file:
.. code:: ini
[common]
database = mydatabase.sqlite
quiet = 2
[fetch]
user = some@email.address.org
password = secret
team = myteam
token = xxxx-1111111111-222222222222-333333333333-r4nd0ms7uff
Note, that only ``[common]`` and ``[fetch]`` sections are provided, so it is
enough to invoke ``slack-backup`` command as:
.. code:: shell-session
(myenv)user@localhost ~/mylogs $ slack-backup fetch
There are couple of places, where configuration file would be searched for, in
particular order:
* file provided via argument ``-i`` or ``--config``
* ``slack-backup.ini`` in current directory
* ``$XDG_CONFIG_HOME/slack-backup.ini``, where ``$XDG_CONFIG_HOME`` usually
defaults to ``$HOME/.config``
Details Details
------- -------

View File

@@ -1,108 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Create backup for certain date for specified channel in slack Execute commands for slack-backup
""" """
import argparse from slack_backup import command
import logging
from slack_backup import client
def setup_logger(args):
"""Setup logger format and level"""
level = logging.WARNING
if args.quiet:
level = logging.ERROR
if args.quiet > 1:
level = logging.CRITICAL
if args.verbose:
level = logging.INFO
if args.verbose > 1:
level = logging.DEBUG
logging.basicConfig(level=level,
format="%(asctime)s %(levelname)s: %(message)s")
def generate_raport(args):
"""Generate logs"""
slack = client.Client(args)
slack.generate_history()
def fetch_data(args):
"""Fetch and store data"""
slack = client.Client(args)
slack.update()
def main():
"""Main function"""
parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(dest='parser')
subparser.required = True
fetch = subparser.add_parser('fetch', help='Update local db with Slack'
' data')
fetch.add_argument('-t', '--token', required=True, help='Slack token - '
'a string, which can be generated/obtained via '
'https://api.slack.com/docs/oauth-test-tokens page.')
fetch.add_argument('-u', '--user', default='', help='Username for your '
'Slack account')
fetch.add_argument('-p', '--password', default='', help='Password for your '
'Slack account.')
fetch.add_argument('-e', '--team', required=True, help='Team name, which '
'is part of slack url, for example: if url is '
'"https://team.slack.com" than "team" is a name of '
'the team.')
fetch.add_argument('-v', '--verbose', help='Be verbose. Adding more "v"'
'will increase verbosity', action="count", default=0)
fetch.add_argument('-q', '--quiet', help='Be quiet. Adding more "q"'
'will decrease verbosity', action="count", default=0)
fetch.add_argument('-c', '--channels', default=[], nargs='+',
help='List of channels to perform actions on. '
'Default is all channels.')
fetch.add_argument('-d', '--database', default='',
help='Path to the database file.')
fetch.set_defaults(func=fetch_data)
generate = subparser.add_parser('generate', help='Generate logs out of '
'data in provided database')
generate.add_argument('-o', '--output', default='logs', help="Output "
"directory for store logs. All logs are organised "
"per channel. By default it's `logs' directory")
generate.add_argument('-f', '--format', default='none',
choices=('text', 'none'),
help='Output format. Default is none; only database '
'is updated by latest messages for all/selected '
'channels.')
generate.add_argument('-t', '--theme', default='plain',
choices=('plain', 'unicode'),
help='Choose theme for text output. It doesn\'t '
'affect other output formats.')
generate.add_argument('-v', '--verbose', help='Be verbose. Adding more "v"'
'will increase verbosity', action="count", default=0)
generate.add_argument('-q', '--quiet', help='Be quiet. Adding more "q"'
'will decrease verbosity', action="count", default=0)
generate.add_argument('-c', '--channels', default=[], nargs='+',
help='List of channels to perform actions on. '
'Default is all channels.')
generate.add_argument('-d', '--database', default='',
help='Path to the database file.')
generate.set_defaults(func=generate_raport)
args = parser.parse_args()
setup_logger(args)
args.func(args)
if __name__ == "__main__": if __name__ == "__main__":
main() command.main()

View File

@@ -2,12 +2,15 @@
""" """
Setup for the slack-backup project Setup for the slack-backup project
""" """
from distutils.core import setup try:
from setuptools import setup
except ImportError:
from distutils.core import setup
setup(name="slack-backup", setup(name="slack-backup",
packages=["slack_backup"], packages=["slack_backup"],
version="0.2", version="0.4.4",
description="Make copy of slack converstaions", description="Make copy of slack converstaions",
author="Roman Dobosz", author="Roman Dobosz",
author_email="gryf73@gmail.com", author_email="gryf73@gmail.com",

118
slack_backup/command.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Create backup for certain date for specified channel in slack
"""
import argparse
import logging
from slack_backup import client
from slack_backup import config
def setup_logger(args):
"""Setup logger format and level"""
level = logging.WARNING
if args.quiet:
level = logging.ERROR
if args.quiet > 1:
level = logging.CRITICAL
if args.verbose:
level = logging.INFO
if args.verbose > 1:
level = logging.DEBUG
logging.basicConfig(level=level,
format="%(asctime)s %(levelname)s: %(message)s")
def generate_raport(args):
"""Generate logs"""
slack = client.Client(args)
slack.generate_history()
def fetch_data(args):
"""Fetch and store data"""
slack = client.Client(args)
slack.update()
def main():
"""Main function"""
parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(dest='parser')
subparser.required = True
fetch = subparser.add_parser('fetch', help='Update local db with Slack'
' data')
fetch.add_argument('-t', '--token', default=None, help='Slack token - '
'a string, which can be generated/obtained via '
'https://api.slack.com/docs/oauth-test-tokens page.')
fetch.add_argument('-u', '--user', default=None, help='Username for your '
'Slack account')
fetch.add_argument('-p', '--password', default=None, help='Password for '
'your Slack account.')
fetch.add_argument('-e', '--team', default=None, help='Team name, which '
'is part of slack url, for example: if url is '
'"https://team.slack.com" than "team" is a name of '
'the team.')
fetch.add_argument('-v', '--verbose', help='Be verbose. Adding more "v" '
'will increase verbosity', action="count",
default=None)
fetch.add_argument('-q', '--quiet', help='Be quiet. Adding more "q" will'
' decrease verbosity', action="count", default=None)
fetch.add_argument('-c', '--channels', default=None, nargs='+',
help='List of channels to perform actions on. '
'Default is all channels.')
fetch.add_argument('-d', '--database', default=None,
help='Path to the database file.')
fetch.add_argument('-i', '--config', default=None,
help='Use specific config file.')
fetch.set_defaults(func=fetch_data)
generate = subparser.add_parser('generate', help='Generate logs out of '
'data in provided database')
generate.add_argument('-o', '--output', default=None, help="Output "
"directory for store logs. All logs are organised "
"per channel. By default it's `logs' directory")
generate.add_argument('-f', '--format', default=None,
choices=('text', 'none'),
help='Output format. Default is none; only database '
'is updated by latest messages for all/selected '
'channels.')
generate.add_argument('-t', '--theme', default=None,
choices=('plain', 'unicode'),
help='Choose theme for text output. It doesn\'t '
'affect other output formats.')
generate.add_argument('-v', '--verbose', help='Be verbose. Adding more '
'"v" will increase verbosity', action="count",
default=None)
generate.add_argument('-q', '--quiet', help='Be quiet. Adding more "q" '
'will decrease verbosity', action="count",
default=None)
generate.add_argument('-c', '--channels', default=[], nargs='+',
help='List of channels to perform actions on. '
'Default is all channels.')
generate.add_argument('-d', '--database', default=None,
help='Path to the database file.')
generate.add_argument('-i', '--config', default=None,
help='Use specific config file.')
generate.set_defaults(func=generate_raport)
args = parser.parse_args()
cfg = config.Config()
msg = cfg.update(args)
setup_logger(args)
logging.info(msg)
args.func(args)

112
slack_backup/config.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Configuration module for slack-backup
"""
import json
import os
import configparser
class Config(object):
"""Configuration keeper"""
ints = ['verbose', 'quiet']
sections = {'common': ['channels', 'database', 'quiet', 'verbose'],
'fetch': ['user', 'password', 'team', 'token'],
'generate': ['output', 'format', 'theme']}
def __init__(self):
"""
Init. Read config, if exists, and update passed argument parser
object.
"""
self.cp = configparser.ConfigParser()
self._options = {'channels': [],
'database': None,
'quiet': 0,
'verbose': 0,
'user': None,
'password': None,
'team': None,
'token': None,
'output': None,
'format': None,
'theme': None}
# This message supposed to be displayed in INFO level. During the time
# of running the code where it should be displayed there is no
# complete information about logging level. Displaying message is
# dependent on the a) config file, b) argument from commandline. Let's
# resolve if user want to have that information or not after merging
# those two sources. If user do not want to see any message in INFO
# level, we shouldn't do so.
self.msg = ''
def update(self, args):
self.load_config(args)
self.parse_loaded_options()
self.update_args(args)
return self.msg
def load_config(self, args):
path = ''
if hasattr(args, 'config') and args.config:
path = args.config
locations = [path,
'./slack-backup.ini',
os.path.expandvars('$XDG_CONFIG_HOME/slack-backup.ini'),
os.path.expandvars('$HOME/.config/slack-backup.ini')]
for location in locations:
if os.path.exists(location):
self.cp.read(location)
self.msg = 'Found configuration file: %s' % location
break
else:
self.msg = 'No configuration file found'
def parse_loaded_options(self):
for section in self.cp.sections():
if section not in self.sections:
continue
for option in self.sections[section]:
if option in self.ints:
val = self.cp.getint(section, option, fallback=0)
elif option == 'channels':
val = self.cp.get(section, option, fallback='[]')
val = json.loads(val)
else:
val = self.cp.get(section, option, fallback=None)
self._options[option] = val
def update_args(self, args):
if 'parser' not in args:
# it doesn't make sense to update args, since no action was
# choosen
return
# special case, re-set information for verbose/quiet options
if 'verbose' in args and args.verbose is not None:
self._options['verbose'] = args.verbose
if self._options['quiet'] is not None:
self._options['quiet'] = 0
if 'quiet' in args and args.quiet is not None:
self._options['quiet'] = args.quiet
if self._options['verbose'] is not None:
self._options['verbose'] = 0
for sec_id in (args.parser, 'common'):
for option in self.sections[sec_id]:
if option in args:
if getattr(args, option) is not None:
continue
setattr(args, option, self._options[option])

View File

@@ -55,8 +55,8 @@ class Download(object):
'file': self._files} 'file': self._files}
if filetype == 'file' and not self._authorized: if filetype == 'file' and not self._authorized:
logging.info("There was no (valid) credentials passed, therefore " logging.warning("There was no (valid) credentials passed, "
"file `%s' cannot be downloaded", url) "therefore file `%s' cannot be downloaded", url)
return return
splitted = url.split('/') splitted = url.split('/')

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
This module contains emoji list This module contains emoji list
""" """

View File

@@ -1,19 +1,14 @@
# -*- coding: utf-8 -*-
""" """
Reporters module. Reporters module.
There are several classes for specific format reporting, and also some of the There are several classes for specific format reporting, and also some of the
slack conversation/convention parsers. slack conversation/convention parsers.
""" """
from __future__ import absolute_import, division, print_function
import os import os
import errno import errno
import html.parser
import logging import logging
import re import re
try:
from html.parser import HTMLParser
except ImportError:
from HTMLParser import HTMLParser
from slack_backup import objects as o from slack_backup import objects as o
from slack_backup import utils from slack_backup import utils
@@ -209,7 +204,6 @@ class TextReporter(Reporter):
"""return formatter for file""" """return formatter for file"""
groups = self._slackid_pat[0].match(msg.text).groupdict() groups = self._slackid_pat[0].match(msg.text).groupdict()
text = msg.text.replace(groups['replace'], '') text = msg.text.replace(groups['replace'], '')
text = self._filter_slackid(msg.text)
filename = msg.file.filepath filename = msg.file.filepath
if filename: if filename:
filename = os.path.relpath(msg.file.filepath, start=self.out) filename = os.path.relpath(msg.file.filepath, start=self.out)
@@ -217,9 +211,8 @@ class TextReporter(Reporter):
filename = msg.file.url filename = msg.file.url
if not filename: if not filename:
logging.warning("There is have a file object, but nothing has " logging.warning("There is a file object, but without filename."
"found. Name of the file object is `%s'", "Name of the file object is `%s'", msg.file.name)
msg.file.name)
filename = msg.file.name filename = msg.file.name
text = self._filter_slackid(text) text = self._filter_slackid(text)
@@ -269,7 +262,7 @@ class TextReporter(Reporter):
def _remove_entities(self, text): def _remove_entities(self, text):
"""replace html entites into appropriate chars""" """replace html entites into appropriate chars"""
text = HTMLParser().unescape(text) return html.parser.HTMLParser().unescape(text)
def _filter_slackid(self, text): def _filter_slackid(self, text):
"""filter out all of the id from slack""" """filter out all of the id from slack"""

View File

@@ -1,8 +1,5 @@
from unittest import TestCase from unittest import TestCase
try: from unittest.mock import MagicMock
from unittest.mock import MagicMock
except ImportError:
from mock import MagicMock
from slack_backup import client from slack_backup import client
from slack_backup import objects as o from slack_backup import objects as o

127
tests/test_config.py Normal file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import tempfile
import os
import unittest
from slack_backup import config
CONF = """\
[common]
channels=["one","two", "three"]
database=dbfname.sqlite
quiet=1
verbose=2
[generate]
output=logs
format=text
theme=plain
[fetch]
user=someuser@address.com
password=secret
team=myteam
token=xxxx-1111111111-222222222222-333333333333-r4nd0ms7uff
"""
class TestConfig(unittest.TestCase):
def setUp(self):
fd, self.confname = tempfile.mkstemp()
os.close(fd)
with open(self.confname, 'w') as fobj:
fobj.write(CONF)
def tearDown(self):
os.unlink(self.confname)
def test_config(self):
self.assertTrue(os.path.exists(self.confname))
self.assertTrue(os.path.isfile(self.confname))
args = argparse.Namespace()
args.config = None
args.parser = 'fetch'
args.verbose = 2
conf = config.Config()
conf.update(args)
self.assertDictEqual(vars(args), {'config': None,
'parser': 'fetch',
'verbose': 2,
'quiet': 0,
'channels': [],
'database': None,
'user': None,
'password': None,
'team': None,
'token': None})
args = argparse.Namespace()
args.config = self.confname
args.parser = 'fetch'
args.verbose = 2
args.quiet = None
args.channels = None
args.database = None
args.user = None
args.password = None
args.team = None
args.token = None
conf = config.Config()
conf.update(args)
self.assertEqual(conf._options['verbose'], 2)
self.assertListEqual(conf._options['channels'],
['one', 'two', 'three'])
self.assertEqual(conf._options['database'], 'dbfname.sqlite')
self.assertEqual(conf._options['user'], 'someuser@address.com')
self.assertDictEqual(vars(args), {'config': self.confname,
'parser': 'fetch',
'verbose': 2,
'quiet': 0,
'channels': ['one', 'two', 'three'],
'database': 'dbfname.sqlite',
'user': 'someuser@address.com',
'password': 'secret',
'team': 'myteam',
'token': 'xxxx-1111111111-'
'222222222222-333333333333-'
'r4nd0ms7uff'})
# override some conf options with commandline
args = argparse.Namespace()
args.config = self.confname
args.parser = 'fetch'
args.verbose = None
args.quiet = 2
args.channels = ['foo']
args.database = None
args.user = 'joe'
args.password = 'ultricies'
args.team = ''
args.token = 'the token'
conf = config.Config()
conf.update(args)
self.assertDictEqual(vars(args), {'config': self.confname,
'parser': 'fetch',
'verbose': 0,
'quiet': 2,
'channels': ['foo'],
'database': 'dbfname.sqlite',
'user': 'joe',
'password': 'ultricies',
'team': '',
'token': 'the token'})

View File

@@ -1,10 +1,5 @@
# -*- coding: utf-8 -*-
from unittest import TestCase from unittest import TestCase
try: from unittest.mock import MagicMock
from unittest.mock import MagicMock
except ImportError:
from mock import MagicMock
from slack_backup import reporters as r from slack_backup import reporters as r
@@ -45,7 +40,3 @@ class TestReporter(TestCase):
text = ('<@U111BBBBB|user2>Praesent vel enim sed eros luctus ' text = ('<@U111BBBBB|user2>Praesent vel enim sed eros luctus '
'imperdiet.\nMauris neque ante, <@U111DDDDD> placerat at, ' 'imperdiet.\nMauris neque ante, <@U111DDDDD> placerat at, '
'mollis vitae, faucibus quis, <@U111CCCCC>leo. Ut feugiat.') 'mollis vitae, faucibus quis, <@U111CCCCC>leo. Ut feugiat.')
# Praesent vel enim sed eros luctus imperdiet. Vivamus urna quam, congue
# vulputate, convallis non, cursus cursus, risus. Quisque aliquet. Donec
# vulputate egestas elit. Morbi dictum, sem sit amet aliquam.

14
tox.ini
View File

@@ -1,5 +1,5 @@
[tox] [tox]
envlist = py27,py34,{py27,py34}-flake8 envlist = py34,py34-flake8
usedevelop = True usedevelop = True
@@ -10,18 +10,6 @@ commands = py.test --cov=slack_backup --cov-report=term-missing
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
[testenv:py27]
usedevelop={[testenv]usedevelop}
setenv = {[testenv]setenv}
commands = {[testenv]commands}
deps = {[testenv]deps}
mock
[testenv:py27-flake8]
basepython = python2.7
deps = flake8
commands = flake8 {posargs}
[testenv:py34-flake8] [testenv:py34-flake8]
basepython = python3.4 basepython = python3.4
deps = flake8 deps = flake8