Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM tiangolo/uwsgi-nginx-flask:python2.7

COPY ./ ./

EXPOSE 5000

RUN pip install -r requirements.txt

CMD gunicorn --timeout 0 -c config.py wsgi
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ Commands:
clean Cleans up any temporary files (including...
```

## Deployment

If for any reason you need to use this tool on a server (maybe for your HR manager to easily use) you can build and deploy a docker image.

Build
`docker build -t slack-export-viewer .`

Run
`docker run -p 5000:5000 -d slack-export-viewer`

### Examples

```
Expand Down
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# config for gunicorn server

bind = '0.0.0.0:5000'
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ click
flask
markdown2
emoji
gunicorn
99 changes: 77 additions & 22 deletions slackviewer/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import os
import flask
from flask import request, redirect, url_for, flash
from slackviewer.archive import extract_archive
from slackviewer.reader import Reader


app = flask.Flask(
Expand All @@ -7,14 +11,22 @@
static_folder="static"
)

app.config["UPLOAD_FOLDER"] = "archives"
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 * 1024 # 2 GB limit

reader = Reader()

# these functions only fire when the route is navigated to

@app.route("/channel/<name>/")
def channel_name(name):
messages = flask._app_ctx_stack.channels[name]
channels = list(flask._app_ctx_stack.channels.keys())
groups = list(flask._app_ctx_stack.groups.keys())
dm_users = list(flask._app_ctx_stack.dm_users)
mpim_users = list(flask._app_ctx_stack.mpim_users)
if not hasattr(reader, 'channels'):
return flask.render_template("404.html")
messages = reader.channels[name]
channels = list(reader.channels.keys())
groups = list(reader.groups.keys())
dm_users = list(reader.dm_users)
mpim_users = list(reader.mpim_users)

return flask.render_template("viewer.html", messages=messages,
name=name.format(name=name),
Expand All @@ -26,11 +38,13 @@ def channel_name(name):

@app.route("/group/<name>/")
def group_name(name):
messages = flask._app_ctx_stack.groups[name]
channels = list(flask._app_ctx_stack.channels.keys())
groups = list(flask._app_ctx_stack.groups.keys())
dm_users = list(flask._app_ctx_stack.dm_users)
mpim_users = list(flask._app_ctx_stack.mpim_users)
if not hasattr(reader, 'channels'):
return flask.render_template("404.html")
messages = reader.groups[name]
channels = list(reader.channels.keys())
groups = list(reader.groups.keys())
dm_users = list(reader.dm_users)
mpim_users = list(reader.mpim_users)

return flask.render_template("viewer.html", messages=messages,
name=name.format(name=name),
Expand All @@ -42,11 +56,13 @@ def group_name(name):

@app.route("/dm/<id>/")
def dm_id(id):
messages = flask._app_ctx_stack.dms[id]
channels = list(flask._app_ctx_stack.channels.keys())
groups = list(flask._app_ctx_stack.groups.keys())
dm_users = list(flask._app_ctx_stack.dm_users)
mpim_users = list(flask._app_ctx_stack.mpim_users)
if not hasattr(reader, 'channels'):
return flask.render_template("404.html")
messages = reader.dms[id]
channels = list(reader.channels.keys())
groups = list(reader.groups.keys())
dm_users = list(reader.dm_users)
mpim_users = list(reader.mpim_users)

return flask.render_template("viewer.html", messages=messages,
id=id.format(id=id),
Expand All @@ -58,11 +74,13 @@ def dm_id(id):

@app.route("/mpim/<name>/")
def mpim_name(name):
messages = flask._app_ctx_stack.mpims[name]
channels = list(flask._app_ctx_stack.channels.keys())
groups = list(flask._app_ctx_stack.groups.keys())
dm_users = list(flask._app_ctx_stack.dm_users)
mpim_users = list(flask._app_ctx_stack.mpim_users)
if reader.channels is None:
return flask.render_template("404.html")
messages = reader.mpims[name]
channels = list(reader.channels.keys())
groups = list(reader.groups.keys())
dm_users = list(reader.dm_users)
mpim_users = list(reader.mpim_users)

return flask.render_template("viewer.html", messages=messages,
name=name.format(name=name),
Expand All @@ -72,9 +90,46 @@ def mpim_name(name):
mpim_users=mpim_users)


@app.route("/")
ALLOWED_EXTENSIONS = set(["zip"])

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constants like this should be at the top of the file


def allowed_file(filename):
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two newlines between all module-level functions

@app.route("/", methods=["GET", "POST"])
def upload():
print('upload')

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the request I have general for these print statements is, can you just clean them up a bit?

if request.method == "POST":
# check if the post request has the file part
print(request.files)
if "archive_file" not in request.files:
print('archive_file not in request.file')
flash("No file part")
return redirect(request.url)
file = request.files["archive_file"]
# if user does not select file, browser also
# submit a empty part without filename
if file.filename == "":

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not file.filename works too

print("file name is empty")
flash("No selected file")
return redirect(request.url)
if file and allowed_file(file.filename):
print(os.path.abspath(file.filename))
filename = os.path.abspath(file.filename) # change this to full file path for where it gets uploaded to
archive_path = extract_archive(filename)
reader.set_path(archive_path)
reader.get_all_messages()
return redirect(url_for("index"))

reader.reset()
return flask.render_template("upload.html", reader=reader)


@app.route("/channel/")
def index():
channels = list(flask._app_ctx_stack.channels.keys())
if not hasattr(reader, 'channels'):
return flask.render_template("404.html")
channels = list(reader.channels.keys())
if "general" in channels:
return channel_name("general")
else:
Expand Down
3 changes: 3 additions & 0 deletions slackviewer/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ def extract_archive(filepath):
print("{} already exists".format(extracted_path))
else:
# Extract zip
print("extracting archive...")
with zipfile.ZipFile(filepath) as zip:
print("{} extracting to {}...".format(filepath, extracted_path))
zip.extractall(path=extracted_path)

print("{} extracted to {}".format(filepath, extracted_path))

# Add additional file with archive info
print("creating meta data for next time...")
create_archive_info(filepath, extracted_path, archive_sha)

return extracted_path
Expand Down Expand Up @@ -100,4 +102,5 @@ def create_archive_info(filepath, extracted_path, archive_sha=None):
) as f:
s = json.dumps(archive_info, ensure_ascii=False)
s = to_unicode(s)
print("writing meta data to file...")
f.write(s)
33 changes: 17 additions & 16 deletions slackviewer/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import webbrowser
import time,sys

import click
import flask

from slackviewer.app import app
from slackviewer.app import app, reader
from slackviewer.archive import extract_archive
from slackviewer.reader import Reader
from slackviewer.utils.click import envvar, flag_ennvar


Expand All @@ -14,17 +13,10 @@ def configure_app(app, archive, debug):
if app.debug:
print("WARNING: DEBUG MODE IS ENABLED!")
app.config["PROPAGATE_EXCEPTIONS"] = True

path = extract_archive(archive)
reader = Reader(path)

top = flask._app_ctx_stack
top.channels = reader.compile_channels()
top.groups = reader.compile_groups()
top.dms = reader.compile_dm_messages()
top.dm_users = reader.compile_dm_users()
top.mpims = reader.compile_mpim_messages()
top.mpim_users = reader.compile_mpim_users()
reader.set_path(path)
reader.get_all_messages()


@click.command()
Expand All @@ -35,6 +27,7 @@ def configure_app(app, archive, debug):
help="Path to your Slack export archive (.zip file or directory)")
@click.option('-I', '--ip', default=envvar('SEV_IP', 'localhost'),
type=click.STRING, help="Host IP to serve your content on")
@click.option('-U', '--upload', is_flag=True, help="Start server to upload your zip")
@click.option('--no-browser', is_flag=True,
default=flag_ennvar("SEV_NO_BROWSER"),
help="If you do not want a browser to open "
Expand All @@ -43,14 +36,22 @@ def configure_app(app, archive, debug):
help="Runs in 'test' mode, i.e., this will do an archive extract, but will not start the server,"
" and immediately quit.")
@click.option('--debug', is_flag=True, default=flag_ennvar("FLASK_DEBUG"))
def main(port, archive, ip, no_browser, test, debug):
if not archive:
def main(port, archive, ip, upload, no_browser, test, debug):
if not archive and not upload:
raise ValueError("Empty path provided for archive")

if upload:
webbrowser.open("http://{}:{}".format(ip, port))
app.run(
host=ip,
port=port
)
return

configure_app(app, archive, debug)

if not no_browser and not test:
webbrowser.open("http://{}:{}".format(ip, port))
webbrowser.open("http://{}:{}/channel".format(ip, port))

if not test:
app.run(
Expand Down
61 changes: 50 additions & 11 deletions slackviewer/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,72 @@ class Reader(object):
Reader object will read all of the archives' data from the json files
"""

def __init__(self, PATH):
self._PATH = PATH
# TODO: Make sure this works
with io.open(os.path.join(self._PATH, "users.json"), encoding="utf8") as f:
self.__USER_DATA = {u["id"]: u for u in json.load(f)}
def __init__(self, PATH=''):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: PATH should be path

if not PATH == '':
self._PATH = PATH
with io.open(os.path.join(self._PATH, "users.json"), encoding="utf8") as f:
self.__USER_DATA = {u["id"]: u for u in json.load(f)}


##################
# Public Methods #
##################

def reset(self):
self.channels = None
self.groups = None
self.dms = None
self.dm_users = None
self.mpims = None
self.mpim_users = None
self._PATH = ''
self.__USER_DATA = None

def get_all_messages(self):
"""
This method is used to call all of the compile methods at once.
"""
self.compile_channels()
self.compile_groups()
self.compile_dm_messages()
self.compile_dm_users()
self.compile_mpim_messages()
self.compile_mpim_users()

def set_path(self, path):
"""
Sets the _PATH and readers the users json file to get the user data
"""
self._PATH = path
with io.open(os.path.join(self._PATH, "users.json"), encoding="utf8") as f:
self.__USER_DATA = {u["id"]: u for u in json.load(f)}

def compile_channels(self):

print("getting channels...")
channel_data = self._read_from_json("channels.json")
channel_names = [c["name"] for c in channel_data.values()]

return self._create_messages(channel_names, channel_data)
self.channels = self._create_messages(channel_names, channel_data)

def compile_groups(self):

print("getting groups...")
group_data = self._read_from_json("groups.json")
group_names = [c["name"] for c in group_data.values()]

return self._create_messages(group_names, group_data)
self.groups = self._create_messages(group_names, group_data)

def compile_dm_messages(self):

print("getting dm messages...")
# Gets list of dm objects with dm ID and array of members ids
dm_data = self._read_from_json("dms.json")
dm_ids = [c["id"] for c in dm_data.values()]

# True is passed here to let the create messages function know that
# it is dm data being passed to it
return self._create_messages(dm_ids, dm_data, True)
self.dms = self._create_messages(dm_ids, dm_data, True)

def compile_dm_users(self):
"""
Expand All @@ -58,6 +91,8 @@ def compile_dm_users(self):

"""

print("getting dm users...")

dm_data = self._read_from_json("dms.json")
dms = dm_data.values()
all_dms_users = []
Expand All @@ -68,15 +103,17 @@ def compile_dm_users(self):
dm_members = {"id": dm["id"], "users": [self.__USER_DATA[m] for m in dm["members"]]}
all_dms_users.append(dm_members)

return all_dms_users
self.dm_users = all_dms_users


def compile_mpim_messages(self):

print("getting mpim messages...")

mpim_data = self._read_from_json("mpims.json")
mpim_names = [c["name"] for c in mpim_data.values()]

return self._create_messages(mpim_names, mpim_data)
self.mpims = self._create_messages(mpim_names, mpim_data)

def compile_mpim_users(self):
"""
Expand All @@ -92,6 +129,8 @@ def compile_mpim_users(self):

"""

print("getting mpim users...")

mpim_data = self._read_from_json("mpims.json")
mpims = [c for c in mpim_data.values()]
all_mpim_users = []
Expand All @@ -100,7 +139,7 @@ def compile_mpim_users(self):
mpim_members = {"name": mpim["name"], "users": [self.__USER_DATA[m] for m in mpim["members"]]}
all_mpim_users.append(mpim_members)

return all_mpim_users
self.mpim_users = all_mpim_users


###################
Expand Down
Loading