├── .gitignore ├── LICENSE ├── README.md ├── app.yaml ├── appengine_config.py ├── config.py ├── cron.yaml ├── index.yaml ├── lib ├── __init__.py ├── batch.py ├── import_zip_log.py ├── models.py ├── search_api.py └── slack_dumper.py ├── main.py ├── main_test.py ├── requirements.txt ├── static ├── css │ └── slack.css ├── img │ └── icons │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png └── robots.txt └── templates ├── index.html └── search.html /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Takami Sato 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kaggler-ja Slack log gatheing system and viwer on GAE 2 | 3 | I develop this tool to dump and view log in [Kaggler-ja slack](https://kaggler-ja.herokuapp.com/). 4 | 5 | This tool works on Google App Engine (GAE). 6 | 7 | Maybe, you can use this on your slack with small change. 8 | 9 | Our demo is here: https://kaggler-ja-slack-archive.appspot.com/ 10 | 11 | # Requirements 12 | 13 | * Python 2.7 14 | 15 | # Usage 16 | 17 | 1. Set up your gcp account and install Google Cloud SDK. 18 | 1. Download third-party libraries to pylib directory `pip install -t pylib -r requirements.txt` 19 | 1. change `config.py` 20 | * APP_NAME: set your app name. (it changes html title and left top workspace name) 21 | 1. Set your Slack API Key as environment variable: SLACK_API_KEY or set the key as Settings.api_key entity on datastore. 22 | * On local development server, you can set environment variable like `dev_appserver.py --env_var SLACK_API_KEY=xxxx app.yaml` 23 | 1. Deploy your app. `gcloud app deploy app.yaml index.yaml cron.yaml`. If you want to try on local, run `dev_appserver.py app.yaml` instead. 24 | 1. Kick first log downloading by accessing http://your_url/cron/job 25 | 1. See your top page http://your_url/ 26 | 27 | ### (optional) Import all messages of public channel (over 10,000 messages) 28 | 29 | 1. Export your slack data. see https://get.slack.help/hc/en-us/articles/201658943-Export-your-workspace-data. 30 | 1. Upload it somewhere 31 | 1. set your file's url to `SLACK_DUMPED_LOG_URL`, and set `ROBUST_IMPORTING_MODE 32 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | 6 | # [START handlers] 7 | handlers: 8 | - url: /static 9 | static_dir: static 10 | 11 | - url: /robots.txt 12 | static_files: static/robots.txt 13 | upload: static/robots.txt 14 | 15 | - url: /.* 16 | script: main.app 17 | 18 | -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START vendor] 16 | from google.appengine.ext import vendor 17 | 18 | # Add any libraries installed in the "lib" folder. 19 | vendor.add('pylib') 20 | # [END vendor] 21 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ Application name """ 4 | APP_NAME = 'kaggler-ja' 5 | 6 | """ the number of message """ 7 | NUM_MASSAGES_PER_PAGE = 50 8 | 9 | """ request how old message to get """ 10 | DAYS_REQUEST_PAST_LOG = 1 11 | 12 | """ Slack exported zip file (https://get.slack.help/hc/en-us/articles/201658943-Export-your-workspace-data) """ 13 | SLACK_DUMPED_LOG_URL = 'https://storage.googleapis.com/kaggler-ja-slack-archive.appspot.com/slack_log.zip' 14 | 15 | """ set True when your messages are imported dually""" 16 | ROBUST_IMPORTING_MODE = False 17 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: "daily get item job" 3 | url: /cron/job 4 | schedule: every 1 hours 5 | 6 | - description: "creating search index for all messages" 7 | url: /cron/create_search_index 8 | schedule: 1 of jan,april,july,oct 00:00 9 | 10 | #- description: "loading log job" 11 | # url: /upload/log 12 | # schedule: every 1 hours -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | - kind: Meassage 3 | properties: 4 | - name: ts 5 | direction: desc 6 | - name: channel_id 7 | direction: asc 8 | - name: user 9 | direction: asc 10 | 11 | - kind: Message 12 | properties: 13 | - name: channel_id 14 | - name: ts 15 | direction: desc 16 | 17 | - kind: Message 18 | properties: 19 | - name: channel_id 20 | - name: ts 21 | 22 | 23 | - kind: Message 24 | properties: 25 | - name: user 26 | - name: ts -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/lib/__init__.py -------------------------------------------------------------------------------- /lib/batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from logging import getLogger 6 | logger = getLogger() 7 | 8 | from lib.slack_dumper import SlackDumper 9 | from lib.models import User, Channel, Message, Settings 10 | 11 | 12 | from config import ROBUST_IMPORTING_MODE 13 | 14 | 15 | def get_slack_data(days=None): 16 | 17 | try: 18 | _get_slack_data(days) 19 | except Exception as e: 20 | import traceback 21 | trc = traceback.format_exc() 22 | logger.error(trc) 23 | print(trc) 24 | raise e 25 | 26 | 27 | def _get_slack_data(days): 28 | settings = Settings.query().get() 29 | try: 30 | api_key = settings.api_key 31 | except AttributeError: 32 | api_key = os.environ.get('SLACK_API_KEY') 33 | if api_key is None: 34 | raise Exception('please set environment variable SLACK_API_KEY') 35 | else: 36 | Settings(api_key=api_key).put() 37 | 38 | sd = SlackDumper(api_key) 39 | ents = set(dir(User)) 40 | for user_data in sd.get_user_list(): 41 | user_id = user_data['id'] 42 | user_data = {k: v for k, v in user_data.items() if k in ents} 43 | user = User.query(User.id == user_id).get() 44 | if user is None: 45 | User(**user_data).put() 46 | else: 47 | for k, v in user_data.items(): 48 | setattr(user, k, v) 49 | user.put() 50 | 51 | ents = set(dir(Channel)) 52 | for channel_data in sd.get_channel_list(): 53 | channel_id = channel_data['id'] 54 | channel_data = {k: v for k, v in channel_data.items() if k in ents} 55 | channel = Channel.query(Channel.id == channel_id).get() 56 | if channel is None: 57 | Channel(**channel_data).put() 58 | else: 59 | for k, v in channel_data.items(): 60 | setattr(channel, k, v) 61 | channel.put() 62 | 63 | ents = set(dir(Message)) 64 | for channel_id, messages in sd.get_channels_histoey(days=days).items(): 65 | for message in messages: 66 | message['channel_id'] = channel_id 67 | user = message.get('user', '') 68 | ts_raw = message['ts'] 69 | ts = float(ts_raw) 70 | message['ts'] = float(ts_raw) 71 | message['ts_raw'] = ts_raw 72 | 73 | message = {k: v for k, v in message.items() if k in ents} 74 | if not ROBUST_IMPORTING_MODE: 75 | msg = Message.query().filter(Message.channel_id == channel_id, Message.user == user, Message.ts == ts).get() 76 | else: 77 | msg = Message.query().filter(Message.channel_id == channel_id, Message.user == 78 | user, Message.ts > ts - 1, Message.ts < ts + 1).get() 79 | if msg is None: 80 | Message(**message).put_with_search_index() 81 | else: 82 | for k, v in message.items(): 83 | setattr(msg, k, v) 84 | msg.put_with_search_index() 85 | 86 | 87 | if __name__ == '__main__': 88 | from logging import StreamHandler, Formatter, FileHandler 89 | log_fmt = Formatter('%(asctime)s %(name)s %(lineno)d [%(levelname)s][%(funcName)s] %(message)s ') 90 | 91 | handler = StreamHandler() 92 | handler.setLevel('INFO') 93 | handler.setFormatter(log_fmt) 94 | logger.setLevel('INFO') 95 | logger.addHandler(handler) 96 | 97 | get_slack_data() 98 | -------------------------------------------------------------------------------- /lib/import_zip_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import json 5 | import zipfile 6 | from io import BytesIO 7 | from logging import getLogger 8 | logger = getLogger() 9 | 10 | from lib.models import User, Channel, Message, Settings 11 | 12 | 13 | def import_zip_log(io_buffer): 14 | 15 | try: 16 | _import_zip_log(io_buffer) 17 | except Exception as e: 18 | import traceback 19 | trc = traceback.format_exc() 20 | logger.error(trc) 21 | print(trc) 22 | raise e 23 | 24 | 25 | def _import_zip_log(io_buffer): 26 | logger.debug('start') 27 | 28 | szd = SlackZipDumpedLog(zipfile.ZipFile(BytesIO(io_buffer.read()))) 29 | 30 | ents = set(dir(User)) 31 | for user_data in szd.users_data: 32 | user_id = user_data['id'] 33 | user_data = {k: v for k, v in user_data.items() if k in ents} 34 | user = User.query(User.id == user_id).get() 35 | if user is None: 36 | User(**user_data).put() 37 | else: 38 | for k, v in user_data.items(): 39 | setattr(user, k, v) 40 | user.put() 41 | logger.debug('user end') 42 | 43 | ents = set(dir(Channel)) 44 | for channel_data in szd.channels_data: 45 | channel_id = channel_data['id'] 46 | channel_data['created'] = int(channel_data['created']) 47 | 48 | channel_data = {k: v for k, v in channel_data.items() if k in ents} 49 | channel = Channel.query(Channel.id == channel_id).get() 50 | if channel is None: 51 | Channel(**channel_data).put() 52 | else: 53 | for k, v in channel_data.items(): 54 | setattr(channel, k, v) 55 | channel.put() 56 | 57 | logger.debug('channel end') 58 | ents = set(dir(Message)) 59 | for channel_id, messages in szd.map_message_data.items(): 60 | for message in messages.values(): 61 | message['channel_id'] = channel_id 62 | user = message.get('user', '') 63 | ts_raw = str(message['ts']) 64 | ts = float(ts_raw) 65 | message['ts'] = ts 66 | message['ts_raw'] = ts_raw 67 | 68 | message = {k: v for k, v in message.items() if k in ents} 69 | msg = Message.query().filter(Message.channel_id == channel_id, Message.user == user, Message.ts == ts).get() 70 | if msg is None: 71 | Message(**message).put_with_search_index() 72 | else: 73 | for k, v in message.items(): 74 | setattr(msg, k, v) 75 | msg.put_with_search_index() 76 | logger.debug('exit') 77 | 78 | 79 | class SlackZipDumpedLog(object): 80 | 81 | CHANNELS_DATA_NAME = 'channels.json' 82 | USERS_DATA_NAME = 'users.json' 83 | 84 | def __init__(self, zipfile_buffer): 85 | self.zipfile_buffer = zipfile_buffer 86 | 87 | # print(self.zipfile_buffer.namelist()) 88 | self.channels_data = self._extract_channels_data() 89 | self.users_data = self._extract_users_data() 90 | self.map_message_data = self._extract_message_data() 91 | 92 | def _extract_channels_data(self): 93 | data = json.loads(self.zipfile_buffer.read(self.CHANNELS_DATA_NAME)) 94 | logger.info('channel num: %s' % len(data)) 95 | return data 96 | 97 | def _extract_users_data(self): 98 | data = json.loads(self.zipfile_buffer.read(self.USERS_DATA_NAME)) 99 | logger.info('user num: %s' % len(data)) 100 | return data 101 | 102 | def _extract_message_data(self): 103 | map_message_data = {} 104 | cnt = 0 105 | for ch_data in self.channels_data: 106 | map_messages = {} 107 | ch_name = ch_data['name'] 108 | channel_id = ch_data['id'] 109 | 110 | list_ch_logs = sorted([p for p in self.zipfile_buffer.namelist() 111 | if re.match('^' + ch_name + '/.*.json', p) is not None]) 112 | for path in list_ch_logs: 113 | msgs = json.loads(self.zipfile_buffer.read(path)) 114 | for msg in msgs: 115 | msg['channel_id'] = channel_id 116 | ts_raw = str(msg['ts']) 117 | ts = float(ts_raw) 118 | msg['ts'] = ts 119 | msg['ts_raw'] = ts_raw 120 | user = msg.get('user', '') 121 | 122 | map_messages[ts_raw, user] = msg # overwrite by newdata 123 | cnt += len(map_messages) 124 | map_message_data[channel_id] = map_messages 125 | logger.info('message channel num: %s, total messeage num: %s' % (len(map_message_data), cnt)) 126 | return map_message_data 127 | 128 | 129 | if __name__ == '__main__': 130 | from logging import StreamHandler, Formatter, FileHandler 131 | log_fmt = Formatter('%(asctime)s %(name)s %(lineno)d [%(levelname)s][%(funcName)s] %(message)s ') 132 | 133 | logger = getLogger(None) 134 | handler = StreamHandler() 135 | handler.setLevel('DEBUG') 136 | handler.setFormatter(log_fmt) 137 | logger.setLevel('DEBUG') 138 | logger.addHandler(handler) 139 | 140 | import zipfile 141 | with zipfile.ZipFile('test.zip') as zipfile_buffer: 142 | szd = SlackZipDumpedLog(zipfile_buffer) 143 | -------------------------------------------------------------------------------- /lib/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | import emoji 4 | 5 | from google.appengine.ext import ndb 6 | 7 | MAP_EMOJI = {k.lower(): v for k, v in emoji.EMOJI_ALIAS_UNICODE.items()} 8 | 9 | 10 | class Settings(ndb.Model): 11 | api_key = ndb.StringProperty() 12 | 13 | 14 | class User(ndb.Model): 15 | 16 | id = ndb.StringProperty() 17 | team_id = ndb.StringProperty() 18 | 19 | name = ndb.StringProperty() 20 | deleted = ndb.BooleanProperty() 21 | 22 | color = ndb.StringProperty() 23 | 24 | real_name = ndb.StringProperty() 25 | 26 | tz = ndb.StringProperty() 27 | 28 | tz_label = ndb.StringProperty() 29 | tz_offset = ndb.IntegerProperty() 30 | 31 | profile = ndb.JsonProperty(indexed=False) 32 | is_admin = ndb.BooleanProperty() 33 | 34 | is_owner = ndb.BooleanProperty() 35 | 36 | is_primary_owner = ndb.BooleanProperty() 37 | 38 | is_restricted = ndb.BooleanProperty() 39 | is_ultra_restricted = ndb.BooleanProperty() 40 | 41 | is_bot = ndb.BooleanProperty() 42 | updated = ndb.IntegerProperty() 43 | is_app_user = ndb.BooleanProperty() 44 | 45 | def get_display_name(self): 46 | try: 47 | user_name = self.profile['display_name'] if self.profile['display_name'] != '' else self.profile['real_name'] 48 | except (TypeError, KeyError): 49 | user_name = '' 50 | if user_name == '': 51 | user_name = self.name 52 | return user_name 53 | 54 | 55 | class Channel(ndb.Model): 56 | 57 | id = ndb.StringProperty() 58 | name = ndb.StringProperty() 59 | is_channle = ndb.BooleanProperty() 60 | created = ndb.IntegerProperty() 61 | 62 | creator = ndb.StringProperty() 63 | is_archived = ndb.BooleanProperty() 64 | is_general = ndb.BooleanProperty() 65 | name_normalized = ndb.StringProperty() 66 | 67 | is_shared = ndb.BooleanProperty() 68 | is_org_shared = ndb.BooleanProperty() 69 | is_member = ndb.BooleanProperty() 70 | is_private = ndb.BooleanProperty() 71 | is_mpim = ndb.BooleanProperty() 72 | members = ndb.StringProperty(repeated=True) 73 | topic = ndb.JsonProperty(indexed=False) 74 | purpose = ndb.JsonProperty(indexed=False) 75 | previous_names = ndb.JsonProperty(indexed=False) 76 | num_members = ndb.IntegerProperty() 77 | 78 | 79 | URL_PATTERN = re.compile(r'<(https?|ftp)(://[\w:;/.?%#&=+-]+)>') 80 | IMG_PATTERN = re.compile(r'<(https?|ftp)(://[\w:;/.?%#&=+-]+)\|(.+?)>') 81 | CHANNEL_PATTERN = re.compile(r'<#([A-Z0-9]+)\|([\w;/.?%#&=+-]+)>') 82 | USER_PATTERN = re.compile(r'<@([A-Z0-9]+)>') 83 | 84 | 85 | class Message(ndb.Model): 86 | channel_id = ndb.StringProperty() 87 | type = ndb.StringProperty() 88 | user = ndb.StringProperty() 89 | text = ndb.StringProperty(indexed=False) 90 | ts = ndb.FloatProperty() 91 | ts_raw = ndb.StringProperty() 92 | reactions = ndb.JsonProperty() 93 | 94 | _user_data = None 95 | 96 | @property 97 | def user_data(self): 98 | if self._user_data is None: 99 | self._user_data = User.query(User.id == self.user).get() 100 | return self._user_data 101 | 102 | def get_channel_name(self): 103 | try: 104 | return Channel.query(Channel.id == self.channel_id).get().name 105 | except Exception: 106 | return '' 107 | 108 | def get_ts_timestamp(self): 109 | try: 110 | return datetime.datetime.fromtimestamp(self.ts).strftime('%Y/%m/%d %H:%M') 111 | except Exception: 112 | return None 113 | 114 | def get_datetime(self): 115 | try: 116 | return datetime.datetime.fromtimestamp(self.ts) 117 | except Exception: 118 | return None 119 | 120 | def get_user_name(self): 121 | if self.user_data is None: 122 | return self.user 123 | 124 | return self.user_data.get_display_name() 125 | 126 | def get_user_img_url(self): 127 | try: 128 | return self.user_data.profile['image_48'] 129 | except (TypeError, KeyError, AttributeError): 130 | return '' 131 | 132 | def get_reactions(self): 133 | ret = [] 134 | for react in self.reactions: 135 | text = u':%s:' % react['name'].replace('-', '_') 136 | text = MAP_EMOJI.get(text, text) 137 | react['name'] = text 138 | ret.append(react) 139 | return ret 140 | 141 | def _conv_url(self, text): 142 | text = URL_PATTERN.sub(r'\1\2', text) 143 | text = IMG_PATTERN.sub(r'\1\2', text) 144 | return text 145 | 146 | def _conv_channel_url(self, text): 147 | return CHANNEL_PATTERN.sub(r'#\2', text) 148 | 149 | def _conv_user_name(self, text): 150 | def _get_name(x): 151 | user_data = User.query(User.id == x.group(1)).get() 152 | if user_data is not None: 153 | user_name = user_data.get_display_name() 154 | else: 155 | user_name = x.group(1) 156 | return user_name 157 | 158 | return USER_PATTERN.sub(lambda x: r'@' + _get_name(x) + '', text) 159 | 160 | def _conv_emoji(self, text): 161 | return emoji.emojize(text, use_aliases=True) 162 | 163 | def get_conved_text(self): 164 | text = self.text 165 | text = self._conv_url(text) 166 | text = self._conv_channel_url(text) 167 | text = self._conv_user_name(text) 168 | text = self._conv_emoji(text) 169 | 170 | text = text.replace('\n', '
') 171 | return text 172 | 173 | def put_with_search_index(self): 174 | from lib.search_api import SearchApiHandler 175 | self.put() 176 | sah = SearchApiHandler() 177 | sah.put_one_document(self) 178 | -------------------------------------------------------------------------------- /lib/search_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.api import search 4 | from lib.models import Message 5 | 6 | from logging import getLogger 7 | logger = getLogger(__name__) 8 | 9 | 10 | class SearchApiHandler(object): 11 | 12 | INDEX_NAME = 'message_index' 13 | MSG_PER_PAGE_NUM = 100 14 | 15 | def __init__(self): 16 | self.index = search.Index(name=self.INDEX_NAME) 17 | 18 | def put_all_documents(self): 19 | logger.debug('enter') 20 | for msg in Message.query(): 21 | self.put_one_document(msg) 22 | logger.debug('exit') 23 | 24 | def put_one_document(self, msg): 25 | doc_id = '{channel_id}_{user}_{ts}'.format(channel_id=msg.channel_id, user=msg.user, ts=int(msg.ts)) 26 | 27 | doc = search.Document( 28 | doc_id=doc_id, 29 | fields=[search.TextField(name='text', value=msg.text), 30 | search.AtomField(name='user_name', value=msg.get_user_name()), 31 | search.AtomField(name='channel_id', value=msg.channel_id), 32 | search.AtomField(name='msg_key', value=str(msg.key.id())), 33 | search.DateField(name='ts', value=msg.get_datetime()), 34 | ] 35 | ) 36 | # Index the document. 37 | try: 38 | self.index.put(doc) 39 | except search.PutError, e: 40 | result = e.results[0] 41 | if result.code == search.OperationResult.TRANSIENT_ERROR: 42 | # possibly retry indexing result.object_id 43 | return self.put_one_document(msg) 44 | except search.Error, e: 45 | # possibly log the failure 46 | logger.error('%s' % e.msg) 47 | raise e 48 | 49 | def search_query(self, query_string, page=0): 50 | 51 | # Create sort options to sort on price and brand. 52 | sort_ts = search.SortExpression( 53 | expression='ts', 54 | direction=search.SortExpression.DESCENDING, 55 | default_value=0) 56 | sort_options = search.SortOptions(expressions=[sort_ts]) 57 | 58 | # Create query options using the sort options and expressions created 59 | # above. 60 | query_options = search.QueryOptions( 61 | limit=self.MSG_PER_PAGE_NUM, 62 | offset=page * self.MSG_PER_PAGE_NUM, 63 | returned_fields=['msg_key'], 64 | sort_options=sort_options) 65 | 66 | # Build the Query and run the search 67 | try: 68 | query = search.Query(query_string=query_string, options=query_options) 69 | except search.QueryError: 70 | return [] 71 | results = self.index.search(query) 72 | 73 | return results 74 | 75 | 76 | if __name__ == '__main__': 77 | pass 78 | -------------------------------------------------------------------------------- /lib/slack_dumper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import json 5 | 6 | import urllib2 7 | 8 | from logging import getLogger 9 | logger = getLogger(__name__) 10 | 11 | 12 | class SlackDumper(object): 13 | USERS_LIST_URL_FORMAT = 'https://slack.com/api/users.list?token={token}&cursor={cursor}&limit=1000' 14 | CHANNELS_LIST_URL_FORMAT = 'https://slack.com/api/channels.list?token={token}&cursor={cursor}&limit=1000' 15 | CHANNELS_HISTORY_URL_FORMAT = ('https://slack.com/api/channels.history?' + 16 | 'token={token}&channel={channel}&latest={latest}&oldest={oldest}&count=100') 17 | 18 | def __init__(self, 19 | api_key): 20 | self.api_key = api_key 21 | 22 | self.list_users = None 23 | self.list_channels = None 24 | 25 | def get_user_list(self): 26 | logger.debug('enter') 27 | if self.list_users is not None: 28 | logger.debug('return cached user list') 29 | return self.list_users 30 | 31 | list_users = [] 32 | cursor = '' 33 | while 1: 34 | url = self.USERS_LIST_URL_FORMAT.format(token=self.api_key, cursor=cursor) 35 | 36 | r = urllib2.urlopen(url) 37 | data = json.loads(r.read()) 38 | 39 | if not data['ok']: 40 | raise Exception('fail to get users.list data') 41 | list_users += data['members'] 42 | cursor = data['response_metadata']['next_cursor'] 43 | if cursor == '': 44 | break 45 | 46 | self.list_users = list_users 47 | 48 | logger.info('get %s members' % len(list_users)) 49 | logger.debug('exit') 50 | return list_users 51 | 52 | def get_channel_list(self): 53 | logger.debug('enter') 54 | if self.list_channels is not None: 55 | logger.debug('return cached channel list') 56 | return self.list_channels 57 | 58 | list_channels = [] 59 | cursor = '' 60 | while 1: 61 | url = self.CHANNELS_LIST_URL_FORMAT.format(token=self.api_key, cursor=cursor) 62 | 63 | r = urllib2.urlopen(url) 64 | data = json.loads(r.read()) 65 | 66 | if not data['ok']: 67 | raise Exception('fail to get channels.list data') 68 | list_channels += data['channels'] 69 | cursor = data['response_metadata']['next_cursor'] 70 | if cursor == '': 71 | break 72 | 73 | self.list_channels = list_channels 74 | logger.info('get %s channels' % len(list_channels)) 75 | logger.debug('exit') 76 | return list_channels 77 | 78 | def get_channels_histoey(self, days=None): 79 | logger.debug('enter') 80 | latest = time.time() 81 | oldest = time.time() - (3600 * 24 * days) if days is not None else 0 82 | 83 | map_channles_hist = {} 84 | for ch_data in self.get_channel_list(): 85 | channel_id = ch_data['id'] 86 | list_channel_hist = self.get_channel_hist(channel_id, latest, oldest) 87 | map_channles_hist[channel_id] = list_channel_hist 88 | logger.debug('exit') 89 | 90 | return map_channles_hist 91 | 92 | def get_channel_hist(self, channel_id, latest, oldest): 93 | list_channel_hist = [] 94 | while 1: 95 | url = self.CHANNELS_HISTORY_URL_FORMAT.format(token=self.api_key, 96 | channel=channel_id, 97 | latest=latest, 98 | oldest=oldest) 99 | 100 | r = urllib2.urlopen(url) 101 | data = json.loads(r.read()) 102 | 103 | if not data['ok']: 104 | raise Exception('fail to get %s channel history data' % channel_id) 105 | list_channel_hist += data['messages'] 106 | if data['has_more']: 107 | latest = data['messages'][-1]['ts'] 108 | else: 109 | break 110 | logger.info('get %s messages in %s' % (len(list_channel_hist), channel_id)) 111 | logger.debug('exit') 112 | return list_channel_hist 113 | 114 | 115 | if __name__ == '__main__': 116 | import os 117 | api_key = os.environ.get('KAGGLER_SLACK_API_KEY') 118 | if api_key is None: 119 | raise Exception('please set environment variable KAGGLER_SLACK_API_KEY') 120 | 121 | sd = SlackDumper(api_key) 122 | # sd.get_user_list() 123 | sd.get_channel_list() 124 | ttt = sd.get_channels_histoey() 125 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import urllib2 4 | import traceback 5 | import logging 6 | logging.getLogger().setLevel(logging.DEBUG) 7 | 8 | from google.appengine.ext import ndb 9 | from flask import Flask, render_template, request, redirect 10 | 11 | from lib.models import User, Channel, Message 12 | from lib.batch import get_slack_data 13 | from lib.import_zip_log import import_zip_log 14 | from lib.search_api import SearchApiHandler 15 | 16 | app = Flask(__name__) 17 | 18 | from config import APP_NAME, NUM_MASSAGES_PER_PAGE, DAYS_REQUEST_PAST_LOG, SLACK_DUMPED_LOG_URL 19 | 20 | 21 | @app.route('/') 22 | def index(): 23 | """ Top Page 24 | """ 25 | 26 | ch = request.args.get('ch') 27 | ch_data = Channel.query().filter(Channel.id == ch).get() 28 | if ch_data is None: 29 | ch_data = Channel.query().order(Channel.created).get() 30 | 31 | try: 32 | ch_name = ch_data.name 33 | ch = ch_data.id 34 | except AttributeError: 35 | # maybe there is no data. try to get log data. 36 | return redirect('/cron/job') 37 | 38 | ts = request.args.get('ts') 39 | try: 40 | ts = float(ts) 41 | except (ValueError, TypeError): 42 | ts = time.time() 43 | 44 | is_forward = request.args.get('type') 45 | try: 46 | is_forward = int(is_forward) 47 | except (ValueError, TypeError): 48 | is_forward = 1 49 | 50 | channels = Channel.query().order(Channel.created).iter() 51 | if is_forward: 52 | messages = Message.query().filter(Message.channel_id == ch, 53 | Message.ts < ts).order(-Message.ts).fetch(NUM_MASSAGES_PER_PAGE) 54 | else: 55 | messages = Message.query().filter(Message.channel_id == ch, 56 | Message.ts > ts).order(Message.ts).fetch(NUM_MASSAGES_PER_PAGE) 57 | messages = [m for m in sorted(messages, key=lambda x: x.ts, reverse=True)] 58 | 59 | if len(messages) > 0: 60 | current_ts = messages[0].ts + 0.01 61 | next_ts = messages[-1].ts 62 | else: 63 | current_ts = ts 64 | next_ts = ts 65 | 66 | return render_template('index.html', 67 | app_name=APP_NAME, 68 | current_ch_name=ch_name, 69 | channels=channels, 70 | messages=messages, 71 | current_ch=ch, 72 | current_ts=current_ts, 73 | next_ts=next_ts, 74 | ) 75 | 76 | 77 | @app.route('/search/') 78 | def search(): 79 | """ search Page 80 | """ 81 | query_string = request.args.get('q') 82 | 83 | channels = Channel.query().order(Channel.created).iter() 84 | page = request.args.get('p') 85 | try: 86 | page = int(page) 87 | except (ValueError, TypeError): 88 | page = 0 89 | 90 | sah = SearchApiHandler() 91 | list_msg = sah.search_query(query_string, page) 92 | 93 | #messages = [Message.query(ndb.Key('message', int(m['msg_key'][0].value))) for m in list_msg] 94 | messages = [ndb.Key(Message, int(m['msg_key'][0].value)).get() for m in list_msg] 95 | return render_template('search.html', 96 | app_name=APP_NAME, 97 | channels=channels, 98 | messages=messages, 99 | query_string=query_string, 100 | page=page, 101 | ) 102 | 103 | 104 | @app.route('/cron/job') 105 | def batch(): 106 | """ Get new messages from API 107 | """ 108 | get_slack_data(days=DAYS_REQUEST_PAST_LOG) 109 | return 'successfully end.', 200 110 | 111 | 112 | @app.route('/cron/create_search_index') 113 | def create_search_index(): 114 | """ Get new messages from API 115 | """ 116 | sah = SearchApiHandler() 117 | sah.put_all_documents() 118 | return 'successfully end.', 200 119 | 120 | 121 | @app.route('/upload/log') 122 | def upload_log(): 123 | """ Import messages from exported zip file. 124 | 125 | set your exported zip file to SLACK_DUMPED_LOG_URL 126 | """ 127 | r = urllib2.urlopen(SLACK_DUMPED_LOG_URL) 128 | import_zip_log(r) 129 | 130 | return 'successfully end.', 200 131 | 132 | 133 | @app.errorhandler(500) 134 | def server_error(e): 135 | # Log the error and stacktrace. 136 | logging.error('An internal error occurred.' + traceback.format_exc()) 137 | return 'An internal error occurred.', 500 138 | -------------------------------------------------------------------------------- /main_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from logging import StreamHandler, Formatter, FileHandler, getLogger 18 | import os 19 | from lib.slack_dumper import SlackDumper 20 | 21 | 22 | logger = getLogger(None) 23 | 24 | 25 | log_fmt = Formatter('%(asctime)s %(name)s %(lineno)d [%(levelname)s][%(funcName)s] %(message)s ') 26 | 27 | handler = StreamHandler() 28 | handler.setLevel('INFO') 29 | handler.setFormatter(log_fmt) 30 | logger.setLevel('INFO') 31 | logger.addHandler(handler) 32 | 33 | 34 | @pytest.fixture 35 | def app(): 36 | import main 37 | main.app.testing = True 38 | return main.app.test_client() 39 | 40 | 41 | def test_get_slack_api(): 42 | api_key = os.environ.get('KAGGLER_SLACK_API_KEY') 43 | if api_key is None: 44 | raise Exception('please set environment variable KAGGLER_SLACK_API_KEY') 45 | 46 | sd = SlackDumper(api_key) 47 | # sd.get_user_list() 48 | a = sd.get_channel_list() 49 | import pdb 50 | pdb.set_trace() 51 | # sd.get_channels_histoey() 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.12.3 2 | requests==2.20.0 3 | Werkzeug<0.13.0,>=0.12.0 4 | emoji==0.5.0 5 | -------------------------------------------------------------------------------- /static/css/slack.css: -------------------------------------------------------------------------------- 1 | body{ 2 | 3 | padding-top: 0px; 4 | background-color:rgba(99, 74, 96, 0.9); 5 | background-size: cover; 6 | 7 | } 8 | 9 | #square { 10 | position: relative; 11 | #min-width: 400px; 12 | height: 100vh; 13 | display: block; 14 | margin: auto; 15 | background: white; 16 | overflow: hidden; 17 | z-index: 1; 18 | } 19 | 20 | #leftSquare { 21 | height: 100vh; 22 | position: relative; 23 | background:#4D394B; 24 | overflow-y: scroll; 25 | overflow-x: hidden; 26 | padding: 10px 10px 0 20px; 27 | } 28 | 29 | #rightSquare { 30 | height: 100vh; 31 | overflow: scroll; 32 | position: relative; 33 | padding: 10px 10px 0 10px; 34 | } 35 | 36 | .light{ 37 | margin-left: 5px; 38 | opacity: 0.5; 39 | } 40 | 41 | .highlight-channel{ 42 | background-color: #4C9689; 43 | } 44 | 45 | #header { 46 | display: block; 47 | background-size: cover; 48 | } 49 | 50 | #footer { 51 | display: block; 52 | background-size: cover; 53 | text-align: center; 54 | } 55 | 56 | 57 | .title{ 58 | color: white; 59 | font-family:"lato"; 60 | font-size: 20px; 61 | font-weight: 800; 62 | padding-left: 10%; 63 | text-align: left; 64 | #z-index: 3; 65 | } 66 | 67 | .subtitle{ 68 | position: absolute; 69 | left: 19%; 70 | top: 6%; 71 | color: white; 72 | font-family:"lato"; 73 | font-size: 13px; 74 | opacity: 0.5; 75 | font-weight: 100; 76 | text-align: center; 77 | z-index: 3; 78 | } 79 | 80 | #channel-title{ 81 | color: #2C2D30; 82 | font-family:"lato"; 83 | font-size: 18px; 84 | font-weight: 800; 85 | text-align: left; 86 | z-index: 3; 87 | } 88 | 89 | .channels{ 90 | #position: absolute; 91 | #left: 13%; 92 | #top: 14%; 93 | width: 100%; 94 | color: white; 95 | font-family:"lato"; 96 | font-size: 13px; 97 | opacity: 0.6; 98 | font-weight: 400; 99 | padding-left: 10%; 100 | #text-align: center; 101 | z-index: 3; 102 | } 103 | 104 | .channel{ 105 | color: white; 106 | white-space: nowrap; 107 | font-family:"lato"; 108 | font-size: 13px; 109 | opacity: 1.0; 110 | font-weight: 100; 111 | padding-left: 15%; 112 | text-align: left; 113 | margin-top: 5px; 114 | margin-bottom: 5px; 115 | } 116 | 117 | 118 | .add{ 119 | position: absolute; 120 | right: -72%; 121 | opacity: 0.6; 122 | top: 5%; 123 | 124 | } 125 | 126 | .add-two{ 127 | position: absolute; 128 | right: -15%; 129 | opacity: 0.6; 130 | top: 5%; 131 | z-index: 3; 132 | 133 | } 134 | 135 | .star{ 136 | padding-left: 3%; 137 | opacity: 0.6; 138 | z-index: 3; 139 | 140 | } 141 | .user{ 142 | padding-left: 4%; 143 | opacity: 0.6; 144 | z-index: 3; 145 | 146 | } 147 | .pin{ 148 | padding-left: 4%; 149 | opacity: 0.4; 150 | z-index: 3; 151 | 152 | } 153 | .phone{ 154 | position: absolute; 155 | top: 45%; 156 | right: 48%; 157 | opacity: 0.6; 158 | z-index: 3; 159 | } 160 | 161 | .cog{ 162 | position: absolute; 163 | top: 45%; 164 | right: 42%; 165 | opacity: 0.6; 166 | z-index: 3; 167 | 168 | } 169 | 170 | .details{ 171 | position: absolute; 172 | top: 45%; 173 | right: 36%; 174 | opacity: 0.6; 175 | z-index: 3; 176 | 177 | } 178 | 179 | .search-glass{ 180 | position: absolute; 181 | top: 43%; 182 | right: 28%; 183 | opacity: 0.5; 184 | z-index: 3; 185 | 186 | } 187 | 188 | .at{ 189 | 190 | position: absolute; 191 | top: 45%; 192 | right: 12%; 193 | opacity: 0.6; 194 | z-index: 3; 195 | } 196 | 197 | .star-o{ 198 | 199 | position: absolute; 200 | top: 45%; 201 | right: 7%; 202 | opacity: 0.6; 203 | z-index: 3; 204 | 205 | } 206 | 207 | .menu{ 208 | 209 | position: absolute; 210 | top: 45%; 211 | right: 3%; 212 | opacity: 0.6; 213 | z-index: 3; 214 | } 215 | 216 | .search{ 217 | position: absolute; 218 | right: 17%; 219 | top: 38%; 220 | height: 30%; 221 | width: 14%; 222 | font-weight: 100; 223 | padding-left: 20px; 224 | font-family:"lato"; 225 | font-size: 12px; 226 | opacity: 0.6; 227 | color: #2C2D30; 228 | } 229 | 230 | .line{ 231 | 232 | position: absolute; 233 | left: 7%; 234 | top: 46.5%; 235 | font-family:"lato"; 236 | font-size: 13px; 237 | opacity: 0.5; 238 | font-weight: 100; 239 | color: #2C2D30; 240 | 241 | } 242 | 243 | .bottom-header-line{ 244 | margin-left: 8px; 245 | width: 98%; 246 | margin-top: 0; 247 | 248 | } 249 | 250 | 251 | .message{ 252 | padding-bottom: 20px; 253 | margin: 0px; 254 | } 255 | 256 | 257 | .bell{ 258 | position: absolute; 259 | color: #D0CDD0; 260 | opacity: 0.6; 261 | top: 5%; 262 | left: 89%; 263 | 264 | } 265 | 266 | .down{ 267 | position: absolute; 268 | color: #D0CDD0; 269 | opacity: 0.3; 270 | top: 4.25%; 271 | left: 80%; 272 | 273 | } 274 | 275 | #message-area{ 276 | padding: 0 20px 0 5px; 277 | } 278 | 279 | .person{ 280 | 281 | } 282 | 283 | .person-img{ 284 | border-radius: 10%; 285 | } 286 | 287 | .person-img-container{ 288 | min-width: 48px; 289 | padding-right: 5px; 290 | padding-left: 0px; 291 | text-align: right; 292 | } 293 | 294 | .person-name{ 295 | color: #2C2D30; 296 | font-family:"lato"; 297 | font-size: 15px; 298 | font-weight: 800; 299 | text-align: left; 300 | } 301 | 302 | 303 | 304 | .time{ 305 | color: #2C2D30; 306 | padding-left: 5px; 307 | font-family:"lato"; 308 | font-size: 12px; 309 | opacity: 0.6; 310 | font-weight: 400; 311 | } 312 | 313 | .message-link{ 314 | padding-left: 5px; 315 | font-family:"lato"; 316 | font-size: 12px; 317 | opacity: 0.7; 318 | font-weight: 400; 319 | } 320 | 321 | .person-text{ 322 | color: #2C2D30; 323 | font-family:"lato"; 324 | font-size: 15px; 325 | margin-bottom:0; 326 | font-weight: 400; 327 | text-align: left; 328 | word-wrap: break-word; 329 | } 330 | 331 | .reaction{ 332 | font-size: 15px; 333 | background-color: #EAF6FD; 334 | color: #00A7EA; 335 | padding: 3px; 336 | margin-right: 5px; 337 | border-radius: 5px; 338 | } 339 | 340 | .link-user{ 341 | background-color: #EAF6FD; 342 | color: #00A7EA; 343 | 344 | } 345 | 346 | .search-bottom{ 347 | position: absolute; 348 | display: block; 349 | margin: auto; 350 | left: 28%; 351 | bottom: 3%; 352 | height: 5%; 353 | width: 70%; 354 | font-weight: 100; 355 | padding-left: 28px; 356 | font-family:"lato"; 357 | font-size: 12px; 358 | opacity: 0.6; 359 | color: #2C2D30; 360 | } 361 | 362 | .smile{ 363 | 364 | position: absolute; 365 | display: block; 366 | margin: auto; 367 | bottom: 4%; 368 | right: 3%; 369 | opacity: 0.6; 370 | color: #2C2D30; 371 | } 372 | 373 | .plus{ 374 | 375 | position: absolute; 376 | display: block; 377 | margin: auto; 378 | bottom: 3.75%; 379 | left: 28.75%; 380 | opacity: 0.3; 381 | color: #2C2D30; 382 | } 383 | 384 | -------------------------------------------------------------------------------- /static/img/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /static/img/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /static/img/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /static/img/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /static/img/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /static/img/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /static/img/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/apple-icon.png -------------------------------------------------------------------------------- /static/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/favicon-96x96.png -------------------------------------------------------------------------------- /static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/favicon.ico -------------------------------------------------------------------------------- /static/img/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /static/img/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /static/img/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /static/img/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /static/img/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkm2261/kaggler_ja_slack_archiver/5bb66e5068fe596e289485b5fc91b7d47f50c84f/static/img/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | 4 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ app_name }} slack Log Viewer{% if ch_name %}: {{ ch_name }}{% endif %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |

{{ app_name }}

45 |
46 |
47 |
48 |

CHANNELS{{ channels.size }}

49 |
50 | {% for channel in channels %} 51 | 52 |
53 |

# {{ channel.name }}

54 |
55 |
56 | {% endfor %} 57 |
58 | 59 |
60 | 103 |
104 |
105 | 106 | 107 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ app_name }} slack Log Viewer: Search {{query_string}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |

{{ app_name }}

45 |
46 |
47 |
48 |

CHANNELS{{ channels.size }}

49 |
50 | {% for channel in channels %} 51 | 52 |
53 |

# {{ channel.name }}

54 |
55 |
56 | {% endfor %} 57 |
58 | 59 |
60 | 107 |
108 |
109 | 110 | 111 | 127 | 128 | 129 | --------------------------------------------------------------------------------