forked from fontanon/andaluh-slack
-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
282 lines (218 loc) · 7.8 KB
/
app.py
1
2
3
4
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import os
import re
from celery import Celery
from flask import Flask, json, request
from flask.ext.cache import Cache
from redis import StrictRedis
import requests
def make_celery(app):
celery = Celery(app.import_name, broker=app.config['BROKER_URL'])
celery.conf.update(app.config)
TaskBase = celery.Task
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
with app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
return celery
def make_app(env):
app = Flask(__name__)
app.debug = 'DEBUG' in os.environ
app.config.update(BROKER_URL=env['REDIS_URL'],
CELERY_RESULT_BACKEND=env['REDIS_URL'])
async = ('ASYNC_TRANSLATION' in env and
env['ASYNC_TRANSLATION'] == 'YES')
app.config.update(CELERY_ALWAYS_EAGER=(False if async else True))
return app
def make_cache(app):
try:
cache = Cache(app, config={
'CACHE_TYPE': 'redis',
'CACHE_KEY_PREFIX': 'slack-translator',
'CACHE_REDIS_URL': app.config['BROKER_URL']
})
except KeyError:
raise RuntimeError('REDIS_URL environment variable is required')
return cache
app = make_app(os.environ)
cache = make_cache(app)
celery = make_celery(app)
redis_store = StrictRedis.from_url(os.environ['REDIS_URL'])
def store_to_redis(key, obj):
"""Save json-serializable object to redis"""
redis_store.set(key, json.dumps(obj))
def load_from_redis(key):
"""Load json-serializable object from redis"""
obj_raw = redis_store.get(key)
if obj_raw is None:
return None
return json.loads(obj_raw)
@cache.memoize(timeout=86400)
def andaluh_translate(text):
return requests.get(
'https://api.andaluh.es/epa',
params=dict(
spanish=text
)
).json()['andaluh']
try:
translate = andaluh_translate
except KeyError:
raise RuntimeError(
'TRANSLATE_ENGINE: there is no {0!r} translate engine'.format(
translate_engine
)
)
assert callable(translate)
@cache.memoize(timeout=86400)
def get_user(user_id):
return requests.get(
'https://slack.com/api/users.info',
params=dict(
token=os.environ['SLACK_API_TOKEN'],
user=user_id
)
).json()['user']
@celery.task()
def translate_and_send(user_id, user_name, channel_name, text):
translated = translate(text)
user = get_user(user_id)
response = requests.post(
os.environ['SLACK_WEBHOOK_URL'],
json={
"username": user_name,
"text": translated,
"mrkdwn": True,
"parse": "full",
"channel": '#' + channel_name,
"icon_url": user['profile']['image_72']
}
)
return response.text
@app.route('/andaluh', methods=['GET', 'POST'])
def index():
translate_and_send.delay(
request.values.get('user_id') or request.json['user_id'],
request.values.get('user_name') or request.json['user_name'],
request.values.get('channel_name') or request.json['channel_name'],
request.values.get('text') or request.json['text']
)
return 'Transliterating: ' + (request.values.get('text') or request.json['text'])
def post_to_slack(**kwargs):
"""Post to slack"""
return requests.post(
os.environ['SLACK_WEBHOOK_URL'],
json={
# Set some sane defaults
'mrkdwn': True,
'parse': 'full',
**kwargs
}
)
def post_to_slack_as_bot(channel_id, text):
"""Post to slack as slack-translator bot"""
return post_to_slack(
text=text,
username='Andaluh for Slack',
channel=channel_id,
icon_emoji=':globe_with_meridians:',
)
def post_to_slack_as_user(user_id, channel_id, text):
"""Post to slack imitating a user"""
user = get_user(user_id)
return post_to_slack(
text=text,
username=user['name'],
channel=channel_id,
icon_url=user['profile']['image_72'],
)
re_korean = re.compile('[가-힣ㄱ-ㅎ]')
re_japanese = re.compile('[ぁ-んァ-ン一-龯]')
def detect_language(text):
"""Detect language of given text. Only supports ko/ja/en."""
korean_score = len(re_korean.findall(text))
japanese_score = len(re_japanese.findall(text))
if korean_score and korean_score >= japanese_score:
return 'ko'
if japanese_score > korean_score:
return 'ja'
return 'en'
def get_meeting_mode_channels():
"""Load meeting mode channels"""
meeting_mode_channels = load_from_redis('meeting_mode_channels')
if meeting_mode_channels is None:
meeting_mode_channels = {}
store_to_redis('meeting_mode_channels', meeting_mode_channels)
return meeting_mode_channels
@app.route('/meeting_mode', methods=['POST'])
def meeting_mode():
"""Handle slack events"""
if 'challenge' in request.json:
# Handle Slack handler url verification
return request.json['challenge']
event = request.json.get('event', {})
if 'bot_id' in event:
# Ignore bot messages
return ''
meeting_mode_channels = get_meeting_mode_channels()
channel_id = event.get('channel')
text = event.get('text')
user_id = event.get('user')
if channel_id in meeting_mode_channels:
lang1 = meeting_mode_channels[channel_id]['language1']
lang2 = meeting_mode_channels[channel_id]['language2']
if detect_language(text) == lang1:
from_, to = lang1, lang2
elif detect_language(text) == lang2:
from_, to = lang2, lang1
else:
# Don't translate if language is undetected.
return ''
translated_text = translate(text, from_, to)
post_to_slack_as_user(user_id, channel_id, translated_text)
return ''
@app.route('/start_meeting_mode/<string:language1>/<string:language2>',
methods=['GET', 'POST'])
def start_meeting_mode(language1, language2):
"""Start meeting mode. Translates all messages in `langauge1`
and `language2` to each other.
:param language1: Preferred language of the callee
:param language2: Other language to translate
"""
channel_id = request.values['channel_id']
user_id = request.values['user_id']
user_name = request.values['user_name']
meeting_mode_channels = get_meeting_mode_channels()
if channel_id in meeting_mode_channels:
message = f'@{user_name}: 이미 회의 모드가 진행중입니다.'
post_to_slack_as_bot(channel_id, message)
return ''
meeting_mode_channels[channel_id] = {
'channel_id': channel_id,
'user_id': user_id,
'language1': language1,
'language2': language2,
}
store_to_redis('meeting_mode_channels', meeting_mode_channels)
message = f'@{user_name}님의 요청으로 회의 모드를 개시합니다. ' \
f'현 시간부로 이 채널의 모든 대화는 번역됩니다.'
post_to_slack_as_bot(channel_id, message)
return ''
@app.route('/stop_meeting_mode/', methods=['GET', 'POST'])
def stop_meeting_mode():
"""Stops meeting mode in current channel"""
channel_id = request.values['channel_id']
user_name = request.values['user_name']
meeting_mode_channels = get_meeting_mode_channels()
if channel_id not in meeting_mode_channels:
message = f'@{user_name}: 진행중인 회의 모드가 없습니다.'
post_to_slack_as_bot(channel_id, message)
return ''
meeting_mode_channels.pop(channel_id)
store_to_redis('meeting_mode_channels', meeting_mode_channels)
message = f'@{user_name}님의 요청으로 회의 모드를 종료합니다.'
post_to_slack_as_bot(channel_id, message)
return ''
if __name__ == '__main__':
app.run(debug=True)