├── webexteamsarchiver ├── __init__.py ├── static │ ├── .images │ │ └── icon.png │ ├── .fonts │ │ ├── Roboto │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-Thin.ttf │ │ │ ├── Roboto-Black.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── Roboto-BlackItalic.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-LightItalic.ttf │ │ │ ├── Roboto-ThinItalic.ttf │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ └── LICENSE.txt │ │ ├── collab-ui-icons-28119b2a06bcd7daada5d307c874de91.woff2 │ │ └── collab-ui-icons-4ad727153f66e210ad5a9f842acccaad.woff │ ├── .css │ │ ├── fonts.css │ │ ├── featherlight.min.css │ │ └── teams.css │ └── .js │ │ ├── featherlight.min.js │ │ └── jquery.min.js ├── templates │ ├── header.html │ ├── default.html │ ├── default.txt │ └── room_content.html ├── jinja_env.py └── webexteamsarchiver.py ├── sample.png ├── docs └── teams.css ├── requirements.txt ├── setup.cfg ├── MANIFEST.in ├── Pipfile ├── LICENSE ├── setup.py ├── .gitignore ├── CHANGELOG ├── README.rst └── Pipfile.lock /webexteamsarchiver/__init__.py: -------------------------------------------------------------------------------- 1 | from .webexteamsarchiver import WebexTeamsArchiver -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/sample.png -------------------------------------------------------------------------------- /docs/teams.css: -------------------------------------------------------------------------------- 1 | @import url("https://teams.webex.com/styles/app-cdf3d50d43c60e94a6bd.css"); -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | webexteamssdk 2 | jinja2==2.11.3 3 | requests 4 | hurry.filesize 5 | bump2version 6 | MarkupSafe==2.0.1 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.11.3 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | -------------------------------------------------------------------------------- /webexteamsarchiver/static/.images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.images/icon.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE CHANGELOG MANIFEST.in 2 | recursive-include webexteamsarchiver/static *.* 3 | recursive-include webexteamsarchiver/templates *.* -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/Roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/Roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/collab-ui-icons-28119b2a06bcd7daada5d307c874de91.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/collab-ui-icons-28119b2a06bcd7daada5d307c874de91.woff2 -------------------------------------------------------------------------------- /webexteamsarchiver/static/.fonts/collab-ui-icons-4ad727153f66e210ad5a9f842acccaad.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/HEAD/webexteamsarchiver/static/.fonts/collab-ui-icons-4ad727153f66e210ad5a9f842acccaad.woff -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | webexteamssdk = "*" 8 | requests = "*" 9 | "hurry.filesize" = "*" 10 | bump2version = "*" 11 | Jinja2 = "==2.11.3" 12 | MarkupSafe = "==2.0.1" 13 | 14 | [dev-packages] 15 | 16 | [requires] 17 | python_version = "3.9" 18 | -------------------------------------------------------------------------------- /webexteamsarchiver/static/.css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "collab-ui-icons"; 3 | src: url(../.fonts/collab-ui-icons-28119b2a06bcd7daada5d307c874de91.woff2) format("woff2"), url(../.fonts/collab-ui-icons-4ad727153f66e210ad5a9f842acccaad.woff) format("woff"); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | 9 | @font-face { 10 | font-family: "Roboto"; 11 | src: url("../.fonts/Roboto/Roboto-Regular.ttf"); 12 | } 13 | -------------------------------------------------------------------------------- /webexteamsarchiver/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ room.title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2021 Cisco and/or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /webexteamsarchiver/static/.css/featherlight.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Featherlight - ultra slim jQuery lightbox 3 | * Version 1.7.13 - http://noelboss.github.io/featherlight/ 4 | * 5 | * Copyright 2018, Noël Raoul Bossart (http://www.noelboss.com) 6 | * MIT Licensed. 7 | **/ 8 | html.with-featherlight{overflow:hidden}.featherlight{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:2147483647;text-align:center;white-space:nowrap;cursor:pointer;background:#333;background:rgba(0,0,0,0)}.featherlight:last-of-type{background:rgba(0,0,0,.8)}.featherlight:before{content:'';display:inline-block;height:100%;vertical-align:middle}.featherlight .featherlight-content{position:relative;text-align:left;vertical-align:middle;display:inline-block;overflow:auto;padding:25px 25px 0;border-bottom:25px solid transparent;margin-left:5%;margin-right:5%;max-height:95%;background:#fff;cursor:auto;white-space:normal}.featherlight .featherlight-inner{display:block}.featherlight link.featherlight-inner,.featherlight script.featherlight-inner,.featherlight style.featherlight-inner{display:none}.featherlight .featherlight-close-icon{position:absolute;z-index:9999;top:0;right:0;line-height:25px;width:25px;cursor:pointer;text-align:center;font-family:Arial,sans-serif;background:#fff;background:rgba(255,255,255,.3);color:#000;border:0;padding:0}.featherlight .featherlight-close-icon::-moz-focus-inner{border:0;padding:0}.featherlight .featherlight-image{width:100%}.featherlight-iframe .featherlight-content{border-bottom:0;padding:0;-webkit-overflow-scrolling:touch}.featherlight iframe{border:0}.featherlight *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@media only screen and (max-width:1024px){.featherlight .featherlight-content{margin-left:0;margin-right:0;max-height:98%;padding:10px 10px 0;border-bottom:10px solid transparent}}@media print{html.with-featherlight>*>:not(.featherlight){display:none}} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import re 4 | from setuptools import setup, find_packages 5 | 6 | __copyright__ = "Copyright (c) 2018 Cisco and/or its affiliates." 7 | __license__ = "MIT" 8 | 9 | 10 | PACKAGE_NAME = 'webexteamsarchiver' 11 | 12 | PACKAGE_KEYWORDS = [ 13 | 'cisco', 14 | 'webex', 15 | 'teams', 16 | 'spark', 17 | 'python', 18 | 'messaging', 19 | ] 20 | 21 | PACKAGE_CLASSIFIERS = [ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: System Administrators', 24 | 'Intended Audience :: Telecommunications Industry', 25 | 'Intended Audience :: Information Technology', 26 | 'Natural Language :: English', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.5', 30 | 'Programming Language :: Python :: 3.6', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Topic :: Communications', 33 | 'Topic :: Communications :: Chat' 34 | ] 35 | 36 | INSTALLATION_REQUIREMENTS = [ 37 | 'webexteamssdk', 38 | 'jinja2', 39 | 'requests', 40 | 'hurry.filesize', 41 | 'bump2version', 42 | ] 43 | 44 | long_description = open( 45 | os.path.join( 46 | os.path.dirname(__file__), 47 | 'README.rst' 48 | ) 49 | ).read() 50 | 51 | setup( 52 | name=PACKAGE_NAME, 53 | author='Felipe de Mello', 54 | author_email='fdemello@cisco.com', 55 | version='0.11.3', 56 | url='https://github.com/CiscoDevNet/webex-teams-archiver', 57 | description='Room archiver utility for Webex Teams', 58 | long_description=long_description, 59 | packages=find_packages('.'), 60 | include_package_data=True, 61 | install_requires=INSTALLATION_REQUIREMENTS, 62 | keywords=' '.join(PACKAGE_KEYWORDS), 63 | classifiers=PACKAGE_CLASSIFIERS, 64 | license='MIT; Copyright (c) 2018 Cisco Systems, Inc.' 65 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Mac files 114 | .DS_Store -------------------------------------------------------------------------------- /webexteamsarchiver/templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | {% include "header.html" %} 3 | 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {% include "room_content.html" %} 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 0.11.3: 2022-03-08 5 | ----------------- 6 | Fixes: 7 | - Froze version for MarkupSafe in order to support Python 3.6 8 | 9 | 10 | 0.11.2: 2021-10-29 11 | ----------------- 12 | Fixes: 13 | - Removed the use of context manager for requests since it's only available after v2.18.0 14 | 15 | 0.11.0: 2020-08-19 16 | ----------------- 17 | New Features: 18 | - Added a initialization parameter for special tokens that have access to all messages in a space. 19 | 20 | 0.10.0: 2020-04-18 21 | ----------------- 22 | New Features: 23 | - Support for threads. 24 | - Added a `compress_folder` option to allow users to choose whether or not to create the compressed archive. 25 | - Removed the `overwrite_folder` option since it had no effect. 26 | 27 | Fixes: 28 | - Text archives were missing a space between the e-mail and timestamp. 29 | 30 | 0.9.2: 2019-09-02 31 | ----------------- 32 | Fixes: 33 | - Removed .DS_Store files from repo. 34 | 35 | 0.9.1: 2019-09-02 36 | ----------------- 37 | New Features: 38 | - Added our own CSS and fonts to enable a true offline archive. 39 | 40 | 0.2.1: 2018-09-26 41 | ----------------- 42 | Fixes: 43 | - Changed spacing and line length to almost comply with PEP8. 44 | 45 | 0.2.0: 2018-09-26 46 | ----------------- 47 | Fixes: 48 | - More docstrings and type hints changes. 49 | - Moved datetime_format back to jinja_env to avoid changing global variable. 50 | 51 | New features: 52 | - Added logging support. 53 | 54 | 0.1.0: 2018-09-25 55 | ----------------- 56 | Fixes: 57 | - Fixed docstrings and type hints. 58 | - Remove async from methods that did not need to be asynchronous. 59 | - Broken jinja template into multiple files to avoid the many levels of indentation. 60 | - Removed assertions in favor of type hints. 61 | 62 | New features: 63 | - Added support to compress folder on finish. 64 | - Added support for custom timestamp. 65 | - Replaced Webex Teams (unlicensed) CSS/fonts with collab-ui CSS/fonts (MIT). 66 | 67 | 0.0.1: 2018-09-24 68 | ----------------- 69 | First commit. 70 | -------------------------------------------------------------------------------- /webexteamsarchiver/templates/default.txt: -------------------------------------------------------------------------------- 1 | Title: {{ room.title }} 2 | Created by {{ room_creator.displayName }} ({{ room_creator.emails[0] }}) on {{ room.created|datetime_format(timestamp_format) }}{% if room.lastActivity %} and last had activity on {{ room.lastActivity|datetime_format(timestamp_format) }}{% endif %}. 3 | 4 | {% for msg in messages %} 5 | {% if not msg.parentId %} 6 | {{- msg.created|datetime_format(timestamp_format) }}{{" "}}{% if msg.personEmail %}{{ msg.personEmail }}{% else %}{{ people.get(msg.personId, {'emails': ['unknown']}).emails[0] }}{% endif %}: {{ msg.text|format_msg(False) }} 7 | {% if msg.files %} 8 | {% for url in msg.files %} 9 | {% if attachments[url].deleted == false %} 10 | {{- msg.created|datetime_format(timestamp_format) }}{{" "}}{% if msg.personEmail %}{{msg.personEmail}}{% else %}{{ people.get(msg.personId, {'emails': ['unknown']}).emails[0] }}{% endif %}: Attachment: {{ attachments[url].filename }} ({{ attachments[url].content_length }} bytes, {{ attachments[url].content_type }}) 11 | {% else %} 12 | Attachment: File deleted or not found. 13 | {% endif %} 14 | {% endfor %} 15 | {% endif %} 16 | {% if msg.id in threads and threads[msg.id]|length > 0 %} 17 | {% for response in threads[msg.id] %} 18 | {{- " (reply) " }}{{- response.created|datetime_format(timestamp_format) }}{{" "}}{% if response.personEmail %}{{response.personEmail}}{% else %}{{ people[response.personId].emails[0] }}{% endif %}: {{ response.text|format_msg(True) }} 19 | {% if response.files %} 20 | {% for url in response.files %} 21 | {% if attachments[url].deleted == false %} 22 | {{- " (reply) " }}{{- response.created|datetime_format(timestamp_format) }}{{" "}}{% if response.personEmail %}{{response.personEmail}}{% else %}{{ people[response.personId].emails[0] }}{% endif %}: Attachment: {{ attachments[url].filename }} ({{ attachments[url].content_length }} bytes, {{ attachments[url].content_type }}) 23 | {% else %} 24 | {{- " (reply) " }}Attachment: File deleted or not found. 25 | {% endif %} 26 | {% endfor %} 27 | {% endif %} 28 | {% endfor %} 29 | {% endif %} 30 | {% endif %} 31 | {% endfor %} -------------------------------------------------------------------------------- /webexteamsarchiver/static/.css/teams.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif !important; 3 | font-weight: 300 !important; 4 | } 5 | 6 | a { 7 | color: #05a1bf !important; 8 | -webkit-font-smoothing: antialiased; 9 | } 10 | 11 | h4 { 12 | font-weight: 300; 13 | } 14 | 15 | /* text */ 16 | 17 | #activities .activity-item { 18 | display: flex; 19 | -ms-flex-align: start; 20 | align-items: flex-start; 21 | margin-top: 1em; 22 | } 23 | 24 | #activities .activity-item--message { 25 | font-size: 16px; 26 | display: inline-block; 27 | text-align: left; 28 | color: #000; 29 | word-wrap: break-word; 30 | white-space: pre-wrap; 31 | max-width: 800px; 32 | width: 100%; 33 | } 34 | 35 | .cui-button.cui-button--none { 36 | border-color: hsla(0, 0%, 90%, 0.88); 37 | border-radius: 50px; 38 | padding: 0; 39 | min-width: 0; 40 | margin-right: 17px; 41 | } 42 | 43 | .cui-avatar.cui-avatar--36 { 44 | width: 2.25rem; 45 | height: 2.25rem; 46 | font-size: 0.7875rem; 47 | line-height: 1.125rem; 48 | } 49 | 50 | .cui-avatar .cui-avatar__letter { 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | width: 100%; 55 | height: 100%; 56 | border-radius: 50px; 57 | background-color: hsla(0, 0%, 90%, 0.88); 58 | } 59 | 60 | .cui-avatar .cui-avatar__img { 61 | border-radius: 50%; 62 | } 63 | 64 | .activity-item-sender-meta { 65 | font-size: 14px; 66 | color: #6b6b6b; 67 | margin-bottom: 0.2em; 68 | } 69 | 70 | /* 71 | .activity-item-sender-meta__publishedDate { 72 | margin-left: 16px; 73 | } */ 74 | 75 | spark-mention { 76 | color: #969696; 77 | } 78 | 79 | #activities .activity-item .content { 80 | padding: 0px 4px; 81 | min-width: 250px !important; 82 | max-width: 90%; 83 | width: 100%; 84 | } 85 | 86 | .activity--reply { 87 | margin: 0 16px 0 68px; 88 | border-left: 4px solid rgba(0, 0, 0, 0.12); 89 | } 90 | 91 | .activity--reply .activity-item { 92 | padding: 10px 0 0; 93 | } 94 | 95 | .activity--reply .activity-item-left-child { 96 | margin: 6px 0 0 16px; 97 | flex: none; 98 | } 99 | 100 | /* ----------------- */ 101 | 102 | /* Files-images */ 103 | 104 | .shareList { 105 | list-style: none; 106 | margin: 10px 0; 107 | padding: 0; 108 | } 109 | 110 | img { 111 | max-width: 100%; 112 | } 113 | 114 | .mid-dot { 115 | margin-right: 10px; 116 | } 117 | 118 | .chip-info { 119 | margin-top: 15px; 120 | padding: 11px; 121 | background-color: #f5f5f5; 122 | font-size: 14px; 123 | border-radius: 5px; 124 | color: #6b6b6b; 125 | } 126 | 127 | .file-chip-secondary { 128 | display: flex; 129 | } 130 | .file-chip-meta-size, 131 | .file-chip-secondary .mid-dot { 132 | flex: 0 0 auto; 133 | } 134 | 135 | /* ---------------------- */ 136 | -------------------------------------------------------------------------------- /webexteamsarchiver/jinja_env.py: -------------------------------------------------------------------------------- 1 | """Jinja Configuration. 2 | 3 | Copyright (c) 2018-2021 Cisco and/or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | """ 21 | import jinja2 22 | import datetime 23 | import re 24 | from hurry.filesize import size, alternative 25 | 26 | __all__ = ['env', 'sanitize_name'] 27 | 28 | 29 | def filesize_format(size_bytes): 30 | if not str(size_bytes).isdigit(): 31 | return 0 32 | 33 | return size(int(size_bytes), system=alternative) 34 | 35 | 36 | def person_letters(display_name: str) -> str: 37 | if not display_name: 38 | return "Person Not Found" 39 | 40 | output = "" 41 | for name in display_name.upper().split(): 42 | output += name[0] 43 | 44 | if len(output) > 2: 45 | return f"{output[0]}{output[-1]}" 46 | 47 | return output 48 | 49 | 50 | def datetime_format(date: datetime, format: str) -> str: 51 | if not date: 52 | return str(date) 53 | 54 | return date.strftime(format) 55 | 56 | 57 | def sanitize_name(text: str) -> str: 58 | text = str(text).strip().replace(' ', '_') 59 | return re.sub(r'(?u)[^-\w.]', '', text) 60 | 61 | 62 | def format_msg(text: str, thread: bool) -> str: 63 | spaces = " " * 2 64 | if thread: 65 | spaces *= 3 66 | 67 | if isinstance(text, str) and "\n" in text: 68 | text = re.sub("\\n", f"\\n{spaces}", text) 69 | text = f"{spaces}{text}" 70 | return f"\n{text}" 71 | 72 | return text 73 | 74 | 75 | env = jinja2.Environment( 76 | autoescape=False, 77 | trim_blocks=True, 78 | lstrip_blocks=True, 79 | keep_trailing_newline=True, 80 | loader=jinja2.PackageLoader('webexteamsarchiver', 'templates') 81 | ) 82 | 83 | env.filters['filesize_format'] = filesize_format 84 | env.filters['person_letters'] = person_letters 85 | env.filters['datetime_format'] = datetime_format 86 | env.filters['sanitize_name'] = sanitize_name 87 | env.filters['format_msg'] = format_msg 88 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Webex Teams Archiver 3 | ===================== 4 | 5 | *Simple utility to archive Webex Teams rooms* 6 | 7 | .. image:: https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg 8 | :target: https://developer.cisco.com/codeexchange/github/repo/CiscoDevNet/webex-teams-archiver 9 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 10 | :target: https://github.com/CiscoDevNet/webex-teams-archiver/blob/master/LICENSE 11 | .. image:: https://img.shields.io/pypi/v/webexteamsarchiver.svg 12 | :target: https://pypi.python.org/pypi/webexteamsarchiver 13 | 14 | ------------------------------------------------------------------------------- 15 | 16 | Webex Teams Archiver extracts the messages and files out of a Webex Teams room and saves them in text, HTML, and JSON formats. 17 | 18 | Example 19 | ------- 20 | 21 | .. code-block:: python 22 | 23 | from webexteamsarchiver import WebexTeamsArchiver 24 | 25 | personal_token = "mytoken" 26 | archiver = WebexTeamsArchiver(personal_token) 27 | 28 | # room id from https://developer.webex.com/docs/api/v1/rooms/list-rooms 29 | room_id = "Y2lzY29zcGFyazovL3VzL1JPT00vd2ViZXh0ZWFtc2FyY2hpdmVy" 30 | archiver.archive_room(room_id) 31 | 32 | Produces the following files: 33 | 34 | .. code-block:: bash 35 | 36 | $ ls 37 | Title_Timestamp.tgz 38 | Title_Timestamp 39 | 40 | $ ls Title_Timestamp/ 41 | Title_Timestamp.html 42 | Title_Timestamp.json 43 | Title_Timestamp.txt 44 | attachments/ 45 | avatars/ 46 | space_details.json 47 | 48 | Below is an example of a simple room that got archived. 49 | 50 | .. image:: https://raw.githubusercontent.com/CiscoDevNet/webex-teams-archiver/master/sample.png 51 | :scale: 40 % 52 | 53 | 54 | Note 1: The HTML version of the archive requires Internet connectivity because of the CSS, which is not packaged with the archive because of licensing conflicts. 55 | 56 | Note 2: Please note that use of the Webex Teams Archiver may violate the retention policy, if any, applicable to your use of Webex Teams. 57 | 58 | Installation 59 | ------------ 60 | 61 | Installing and upgrading is easy: 62 | 63 | **Install via PIP** 64 | 65 | .. code-block:: bash 66 | 67 | $ pip install webexteamsarchiver 68 | 69 | **Upgrading to the latest Version** 70 | 71 | .. code-block:: bash 72 | 73 | $ pip install webexteamsarchiver --upgrade 74 | 75 | Options 76 | ------- 77 | 78 | The `archive_room` method exposes the following options: 79 | 80 | +----------------------+-------------------+---------------------------------------------------+ 81 | | Argument | Default Value | Description | 82 | +======================+===================+===================================================+ 83 | | text_format | True | Create a text version of the archive | 84 | +----------------------+-------------------+---------------------------------------------------+ 85 | | html_format | True | Create an HTML version of the archive | 86 | +----------------------+-------------------+---------------------------------------------------+ 87 | | json_format | True | Create a JSON version of the archive | 88 | +----------------------+-------------------+---------------------------------------------------+ 89 | 90 | 91 | In addition, the `options` kwargs supports the following additional options today: 92 | 93 | +----------------------+-------------------+---------------------------------------------------+ 94 | | Argument | Default Value | Description | 95 | +======================+===================+===================================================+ 96 | | compress_folder | True | Compress archive folder | 97 | +----------------------+-------------------+---------------------------------------------------+ 98 | | delete_folder | False | Delete the archive folder when done | 99 | +----------------------+-------------------+---------------------------------------------------+ 100 | | reverse_order | True | Order messages by most recent on the bottom | 101 | +----------------------+-------------------+---------------------------------------------------+ 102 | | download_attachments | True | Download attachments sent to the room | 103 | +----------------------+-------------------+---------------------------------------------------+ 104 | | download_avatars | True | Download avatar images | 105 | +----------------------+-------------------+---------------------------------------------------+ 106 | | download_workers | 15 | Number of download workers for downloading files | 107 | +----------------------+-------------------+---------------------------------------------------+ 108 | | timestamp_format | %Y-%m-%dT%H:%M:%S | Timestamp strftime format | 109 | +----------------------+-------------------+---------------------------------------------------+ 110 | | file_format | gztar | Archive file format_ | 111 | +----------------------+-------------------+---------------------------------------------------+ 112 | 113 | Questions, Support & Discussion 114 | ------------------------------- 115 | 116 | webexteamsarchiver_ is a *community developed* and *community supported* project. Feedback, thoughts, questions, issues can be submitted using the issues_ page. 117 | 118 | Contribution 119 | ------------ 120 | 121 | webexteamsarchiver_ is a *community developed* project. Code contributions are welcome via PRs! 122 | 123 | *Copyright (c) 2018-2021 Cisco and/or its affiliates.* 124 | 125 | 126 | .. _webexteamsarchiver: https://github.com/CiscoDevNet/webex-teams-archiver 127 | .. _issues: https://github.com/CiscoDevNet/webex-teams-archiver/issues 128 | .. _format: https://docs.python.org/3/library/shutil.html#shutil.make_archive 129 | -------------------------------------------------------------------------------- /webexteamsarchiver/static/.js/featherlight.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Featherlight - ultra slim jQuery lightbox 3 | * Version 1.7.13 - http://noelboss.github.io/featherlight/ 4 | * 5 | * Copyright 2018, Noël Raoul Bossart (http://www.noelboss.com) 6 | * MIT Licensed. 7 | **/ 8 | !function(a){"use strict";function b(a,c){if(!(this instanceof b)){var d=new b(a,c);return d.open(),d}this.id=b.id++,this.setup(a,c),this.chainCallbacks(b._callbackChain)}function c(a,b){var c={};for(var d in a)d in b&&(c[d]=a[d],delete a[d]);return c}function d(a,b){var c={},d=new RegExp("^"+b+"([A-Z])(.*)");for(var e in a){var f=e.match(d);if(f){var g=(f[1]+f[2].replace(/([A-Z])/g,"-$1")).toLowerCase();c[g]=a[e]}}return c}if("undefined"==typeof a)return void("console"in window&&window.console.info("Too much lightness, Featherlight needs jQuery."));if(a.fn.jquery.match(/-ajax/))return void("console"in window&&window.console.info("Featherlight needs regular jQuery, not the slim version."));var e=[],f=function(b){return e=a.grep(e,function(a){return a!==b&&a.$instance.closest("body").length>0})},g={allow:1,allowfullscreen:1,frameborder:1,height:1,longdesc:1,marginheight:1,marginwidth:1,mozallowfullscreen:1,name:1,referrerpolicy:1,sandbox:1,scrolling:1,src:1,srcdoc:1,style:1,webkitallowfullscreen:1,width:1},h={keyup:"onKeyUp",resize:"onResize"},i=function(c){a.each(b.opened().reverse(),function(){return c.isDefaultPrevented()||!1!==this[h[c.type]](c)?void 0:(c.preventDefault(),c.stopPropagation(),!1)})},j=function(c){if(c!==b._globalHandlerInstalled){b._globalHandlerInstalled=c;var d=a.map(h,function(a,c){return c+"."+b.prototype.namespace}).join(" ");a(window)[c?"on":"off"](d,i)}};b.prototype={constructor:b,namespace:"featherlight",targetAttr:"data-featherlight",variant:null,resetCss:!1,background:null,openTrigger:"click",closeTrigger:"click",filter:null,root:"body",openSpeed:250,closeSpeed:250,closeOnClick:"background",closeOnEsc:!0,closeIcon:"✕",loading:"",persist:!1,otherClose:null,beforeOpen:a.noop,beforeContent:a.noop,beforeClose:a.noop,afterOpen:a.noop,afterContent:a.noop,afterClose:a.noop,onKeyUp:a.noop,onResize:a.noop,type:null,contentFilters:["jquery","image","html","ajax","iframe","text"],setup:function(b,c){"object"!=typeof b||b instanceof a!=!1||c||(c=b,b=void 0);var d=a.extend(this,c,{target:b}),e=d.resetCss?d.namespace+"-reset":d.namespace,f=a(d.background||['
','
','",'
'+d.loading+"
","
","
"].join("")),g="."+d.namespace+"-close"+(d.otherClose?","+d.otherClose:"");return d.$instance=f.clone().addClass(d.variant),d.$instance.on(d.closeTrigger+"."+d.namespace,function(b){if(!b.isDefaultPrevented()){var c=a(b.target);("background"===d.closeOnClick&&c.is("."+d.namespace)||"anywhere"===d.closeOnClick||c.closest(g).length)&&(d.close(b),b.preventDefault())}}),this},getContent:function(){if(this.persist!==!1&&this.$content)return this.$content;var b=this,c=this.constructor.contentFilters,d=function(a){return b.$currentTarget&&b.$currentTarget.attr(a)},e=d(b.targetAttr),f=b.target||e||"",g=c[b.type];if(!g&&f in c&&(g=c[f],f=b.target&&e),f=f||d("href")||"",!g)for(var h in c)b[h]&&(g=c[h],f=b[h]);if(!g){var i=f;if(f=null,a.each(b.contentFilters,function(){return g=c[this],g.test&&(f=g.test(i)),!f&&g.regex&&i.match&&i.match(g.regex)&&(f=i),!f}),!f)return"console"in window&&window.console.error("Featherlight: no content filter found "+(i?' for "'+i+'"':" (no target specified)")),!1}return g.process.call(b,f)},setContent:function(b){return this.$instance.removeClass(this.namespace+"-loading"),this.$instance.toggleClass(this.namespace+"-iframe",b.is("iframe")),this.$instance.find("."+this.namespace+"-inner").not(b).slice(1).remove().end().replaceWith(a.contains(this.$instance[0],b[0])?"":b),this.$content=b.addClass(this.namespace+"-inner"),this},open:function(b){var c=this;if(c.$instance.hide().appendTo(c.root),!(b&&b.isDefaultPrevented()||c.beforeOpen(b)===!1)){b&&b.preventDefault();var d=c.getContent();if(d)return e.push(c),j(!0),c.$instance.fadeIn(c.openSpeed),c.beforeContent(b),a.when(d).always(function(a){c.setContent(a),c.afterContent(b)}).then(c.$instance.promise()).done(function(){c.afterOpen(b)})}return c.$instance.detach(),a.Deferred().reject().promise()},close:function(b){var c=this,d=a.Deferred();return c.beforeClose(b)===!1?d.reject():(0===f(c).length&&j(!1),c.$instance.fadeOut(c.closeSpeed,function(){c.$instance.detach(),c.afterClose(b),d.resolve()})),d.promise()},resize:function(a,b){if(a&&b){this.$content.css("width","").css("height","");var c=Math.max(a/(this.$content.parent().width()-1),b/(this.$content.parent().height()-1));c>1&&(c=b/Math.floor(b/c),this.$content.css("width",""+a/c+"px").css("height",""+b/c+"px"))}},chainCallbacks:function(b){for(var c in b)this[c]=a.proxy(b[c],this,a.proxy(this[c],this))}},a.extend(b,{id:0,autoBind:"[data-featherlight]",defaults:b.prototype,contentFilters:{jquery:{regex:/^[#.]\w/,test:function(b){return b instanceof a&&b},process:function(b){return this.persist!==!1?a(b):a(b).clone(!0)}},image:{regex:/\.(png|jpg|jpeg|gif|tiff?|bmp|svg)(\?\S*)?$/i,process:function(b){var c=this,d=a.Deferred(),e=new Image,f=a('');return e.onload=function(){f.naturalWidth=e.width,f.naturalHeight=e.height,d.resolve(f)},e.onerror=function(){d.reject(f)},e.src=b,d.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(b){return a(b)}},ajax:{regex:/./,process:function(b){var c=a.Deferred(),d=a("
").load(b,function(a,b){"error"!==b&&c.resolve(d.contents()),c.fail()});return c.promise()}},iframe:{process:function(b){var e=new a.Deferred,f=a("