You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-27 23:18:38 +02:00
feat: support sync users from slack & rename im to slack
This commit is contained in:
committed by
Daniel Wang
parent
fc12d9b671
commit
8a54b06195
@@ -71,3 +71,10 @@ iris_plan_integration:
|
|||||||
api_key: magic
|
api_key: magic
|
||||||
api_host: http://localhost:16649
|
api_host: http://localhost:16649
|
||||||
plan_url: '/v0/applications/oncall/plans'
|
plan_url: '/v0/applications/oncall/plans'
|
||||||
|
|
||||||
|
|
||||||
|
# slack:
|
||||||
|
# oauth_access_token: 'foo'
|
||||||
|
#
|
||||||
|
# user_sync:
|
||||||
|
# module: 'oncall.user_sync.slack'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE SCHEMA IF NOT EXISTS `oncall` DEFAULT CHARACTER SET utf8 ;
|
CREATE DATABASE IF NOT EXISTS `oncall` DEFAULT CHARACTER SET utf8 ;
|
||||||
USE `oncall`;
|
USE `oncall`;
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
@@ -245,6 +245,11 @@ CREATE TABLE IF NOT EXISTS `contact_mode` (
|
|||||||
`name` varchar(255) NOT NULL,
|
`name` varchar(255) NOT NULL,
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
);
|
);
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Initialize contact modes
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
INSERT INTO `contact_mode` (`name`)
|
||||||
|
VALUES ('email'), ('sms'), ('call'), ('slack');
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
-- Table `user_contact`
|
-- Table `user_contact`
|
||||||
@@ -367,10 +372,8 @@ CREATE TABLE IF NOT EXISTS `application` (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
-- Initialize contact modes, notification types
|
-- Initialize notification types
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
INSERT IGNORE INTO contact_mode VALUES (8, 'sms'), (17, 'im'), (26, 'call'), (35, 'email');
|
|
||||||
|
|
||||||
INSERT INTO `notification_type` (`name`, `subject`, `body`, `is_reminder`)
|
INSERT INTO `notification_type` (`name`, `subject`, `body`, `is_reminder`)
|
||||||
VALUES ('oncall_reminder',
|
VALUES ('oncall_reminder',
|
||||||
'Reminder: oncall shift for %(team)s starts in %(time_before)s',
|
'Reminder: oncall shift for %(team)s starts in %(time_before)s',
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -32,10 +32,12 @@ setup(
|
|||||||
'python-ldap',
|
'python-ldap',
|
||||||
'pytz',
|
'pytz',
|
||||||
'irisclient',
|
'irisclient',
|
||||||
|
'slackclient',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'oncall-dev = oncall.bin.run_server:main',
|
'oncall-dev = oncall.bin.run_server:main',
|
||||||
|
'oncall-user-sync = oncall.bin.user_sync:main',
|
||||||
'build_assets = oncall.bin.build_assets:main',
|
'build_assets = oncall.bin.build_assets:main',
|
||||||
'oncall-scheduler = oncall.bin.scheduler:main',
|
'oncall-scheduler = oncall.bin.scheduler:main',
|
||||||
'oncall-notifier = oncall.bin.notifier:main'
|
'oncall-notifier = oncall.bin.notifier:main'
|
||||||
|
|||||||
49
src/oncall/bin/user_sync.py
Normal file
49
src/oncall/bin/user_sync.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
from gevent import monkey, spawn
|
||||||
|
monkey.patch_all() # NOQA
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
from oncall import utils, db
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger():
|
||||||
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||||
|
|
||||||
|
log_file = os.environ.get('USER_SYNC_LOG_FILE')
|
||||||
|
if log_file:
|
||||||
|
ch = logging.handlers.RotatingFileHandler(
|
||||||
|
log_file, mode='a', maxBytes=10485760, backupCount=10)
|
||||||
|
else:
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(logging.DEBUG)
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
setup_logger()
|
||||||
|
config = utils.read_config(sys.argv[1])
|
||||||
|
user_sync_config = config.get('user_sync')
|
||||||
|
if not user_sync_config:
|
||||||
|
sys.exit('user_sync config not found!')
|
||||||
|
|
||||||
|
sync_module = user_sync_config.get('module')
|
||||||
|
if not sync_module:
|
||||||
|
sys.exit('user_sync module not found!')
|
||||||
|
|
||||||
|
db.init(config['db'])
|
||||||
|
spawn(importlib.import_module(sync_module).main, config).join()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -20,6 +20,7 @@ var oncall = {
|
|||||||
modes: [
|
modes: [
|
||||||
'email',
|
'email',
|
||||||
'sms',
|
'sms',
|
||||||
|
'slack',
|
||||||
'call'
|
'call'
|
||||||
],
|
],
|
||||||
timezones: [
|
timezones: [
|
||||||
|
|||||||
@@ -420,7 +420,7 @@
|
|||||||
Call: <a href="tel:{{getUserInfo this @root.users 'contacts.call'}}"> {{getUserInfo this @root.users 'contacts.call'}} </a>
|
Call: <a href="tel:{{getUserInfo this @root.users 'contacts.call'}}"> {{getUserInfo this @root.users 'contacts.call'}} </a>
|
||||||
<ul class="card-inner-extra">
|
<ul class="card-inner-extra">
|
||||||
<li> SMS: <a href="tel:{{getUserInfo this @root.users 'contacts.sms'}}"> {{getUserInfo this @root.users 'contacts.sms'}} </a> </li>
|
<li> SMS: <a href="tel:{{getUserInfo this @root.users 'contacts.sms'}}"> {{getUserInfo this @root.users 'contacts.sms'}} </a> </li>
|
||||||
<li> Slack: <a>{{getUserInfo this @root.users 'contacts.im'}}</a> </li>
|
<li> Slack: <a>{{getUserInfo this @root.users 'contacts.slack'}}</a> </li>
|
||||||
<li> E-Mail: <a target="_blank" href="mailto:{{getUserInfo this @root.users 'contacts.email'}}"> {{getUserInfo this @root.users 'contacts.email'}} </a> </li>
|
<li> E-Mail: <a target="_blank" href="mailto:{{getUserInfo this @root.users 'contacts.email'}}"> {{getUserInfo this @root.users 'contacts.email'}} </a> </li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -472,7 +472,7 @@
|
|||||||
<ul class="card-inner-extra light small">
|
<ul class="card-inner-extra light small">
|
||||||
<li> Call: {{#if user_contacts.call}} <a href="tel:{{user_contacts.call}}"> {{user_contacts.call}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
<li> Call: {{#if user_contacts.call}} <a href="tel:{{user_contacts.call}}"> {{user_contacts.call}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
||||||
<li> SMS: {{#if user_contacts.sms}} <a href="tel:{{user_contacts.sms}}"> {{user_contacts.sms}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
<li> SMS: {{#if user_contacts.sms}} <a href="tel:{{user_contacts.sms}}"> {{user_contacts.sms}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
||||||
<li> Slack: <a>{{user_contacts.im}}</a> </li>
|
<li> Slack: <a>{{user_contacts.slack}}</a> </li>
|
||||||
<li> E-Mail: <a target="_blank" href="mailto:{{user_contacts.email}}"> {{user_contacts.email}} </a> </li>
|
<li> E-Mail: <a target="_blank" href="mailto:{{user_contacts.email}}"> {{user_contacts.email}} </a> </li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="border-top card-content">
|
<ul class="border-top card-content">
|
||||||
@@ -501,7 +501,7 @@
|
|||||||
<span class="badge pull-right" data-role="{{role}}">{{role}}</span>
|
<span class="badge pull-right" data-role="{{role}}">{{role}}</span>
|
||||||
<ul class="card-inner-extra light small">
|
<ul class="card-inner-extra light small">
|
||||||
<li> SMS: <a href="tel:{{user_contacts.sms}}"> {{user_contacts.sms}} </a> </li>
|
<li> SMS: <a href="tel:{{user_contacts.sms}}"> {{user_contacts.sms}} </a> </li>
|
||||||
<li> Slack: <a>{{user_contacts.im}}</a> </li>
|
<li> Slack: <a>{{user_contacts.slack}}</a> </li>
|
||||||
<li> E-Mail: <a target="_blank" href="mailto:{{user_contacts.email}}"> {{user_contacts.email}} </a> </li>
|
<li> E-Mail: <a target="_blank" href="mailto:{{user_contacts.email}}"> {{user_contacts.email}} </a> </li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="border-top card-content">
|
<ul class="border-top card-content">
|
||||||
@@ -980,7 +980,7 @@
|
|||||||
<li> <a target="_blank" href="mailto:{{user.contacts.email}}"> {{user.contacts.email}} </a> </li>
|
<li> <a target="_blank" href="mailto:{{user.contacts.email}}"> {{user.contacts.email}} </a> </li>
|
||||||
<li> <span class="light"> Call </span> {{#if user.contacts.call}} <a href="tel:{{user.contacts.call}}" target="_blank"> {{user.contacts.call}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
<li> <span class="light"> Call </span> {{#if user.contacts.call}} <a href="tel:{{user.contacts.call}}" target="_blank"> {{user.contacts.call}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
||||||
<li> <span class="light"> SMS </span> {{#if user.contacts.sms}} <a href="tel:{{user.contacts.sms}}" target="_blank"> {{user.contacts.sms}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
<li> <span class="light"> SMS </span> {{#if user.contacts.sms}} <a href="tel:{{user.contacts.sms}}" target="_blank"> {{user.contacts.sms}} </a> {{else}} <span class="error-text">No number found!</span> {{/if}} </li>
|
||||||
<li> <span class="light"> Slack </span> <a> {{user.contacts.im}} <a/> </li>
|
<li> <span class="light"> Slack </span> <a> {{user.contacts.slack}} <a/> </li>
|
||||||
<span class="close remove-card-item" data-admin-action="true" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.info.removeCardItem" data-modal-title="Remove {{user.full_name}}" data-modal-content="Delete {{user.full_name}} from roster?">
|
<span class="close remove-card-item" data-admin-action="true" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.info.removeCardItem" data-modal-title="Remove {{user.full_name}}" data-modal-content="Delete {{user.full_name}} from roster?">
|
||||||
<i class="svg-icon svg-icon-trash">
|
<i class="svg-icon svg-icon-trash">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
||||||
@@ -1001,7 +1001,7 @@
|
|||||||
<li> <a target="_blank" href="mailto:{{getUserInfo this @root.users 'contacts.email'}}"> {{getUserInfo this @root.users 'contacts.email'}} </a> </li>
|
<li> <a target="_blank" href="mailto:{{getUserInfo this @root.users 'contacts.email'}}"> {{getUserInfo this @root.users 'contacts.email'}} </a> </li>
|
||||||
<li> <span class="light"> Call </span> <a href="tel:{{getUserInfo this @root.users 'contacts.call'}}"> {{getUserInfo this @root.users 'contacts.call'}} </a> </li>
|
<li> <span class="light"> Call </span> <a href="tel:{{getUserInfo this @root.users 'contacts.call'}}"> {{getUserInfo this @root.users 'contacts.call'}} </a> </li>
|
||||||
<li> <span class="light"> SMS </span> <a href="tel:{{getUserInfo this @root.users 'contacts.sms'}}"> {{getUserInfo this @root.users 'contacts.sms'}} </a> </li>
|
<li> <span class="light"> SMS </span> <a href="tel:{{getUserInfo this @root.users 'contacts.sms'}}"> {{getUserInfo this @root.users 'contacts.sms'}} </a> </li>
|
||||||
<li> <span class="light"> Slack </span> <a> {{getUserInfo this @root.users 'contacts.im'}} </a></li>
|
<li> <span class="light"> Slack </span> <a> {{getUserInfo this @root.users 'contacts.slack'}} </a></li>
|
||||||
<span class="close remove-card-item" data-admin-action="true" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.info.removeCardItem" data-modal-title="Remove {{getUserInfo this @root.users 'full_name'}}" data-modal-content="Delete {{getUserInfo this @root.users 'full_name'}} from roster?">
|
<span class="close remove-card-item" data-admin-action="true" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.info.removeCardItem" data-modal-title="Remove {{getUserInfo this @root.users 'full_name'}}" data-modal-content="Delete {{getUserInfo this @root.users 'full_name'}} from roster?">
|
||||||
<i class="svg-icon svg-icon-trash">
|
<i class="svg-icon svg-icon-trash">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
||||||
@@ -1113,7 +1113,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Slack ID</td>
|
<td>Slack ID</td>
|
||||||
<td>
|
<td>
|
||||||
<span>{{contacts.im}}</span>
|
<span>{{contacts.slack}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
0
src/oncall/user_sync/__init__.py
Normal file
0
src/oncall/user_sync/__init__.py
Normal file
124
src/oncall/user_sync/slack.py
Normal file
124
src/oncall/user_sync/slack.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
import time
|
||||||
|
from gevent import sleep
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from phonenumbers import format_number, parse, PhoneNumberFormat
|
||||||
|
from slackclient import SlackClient
|
||||||
|
from oncall import db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_phone_number(num):
|
||||||
|
return format_number(parse(num.decode('utf-8'), 'US'), PhoneNumberFormat.INTERNATIONAL)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_oncall_usernames(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('SELECT `name` FROM `user`')
|
||||||
|
users = [row[0] for row in cursor]
|
||||||
|
cursor.close()
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def insert_users(connection, slack_users, users_to_insert, mode_ids):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
for username in users_to_insert:
|
||||||
|
user_info = slack_users[username]
|
||||||
|
cursor.execute(
|
||||||
|
'''INSERT INTO `user` (`name`, `full_name`, `photo_url`)
|
||||||
|
VALUES (%s, %s, %s)''',
|
||||||
|
(username, user_info['full_name'], user_info['photo_url']))
|
||||||
|
connection.commit()
|
||||||
|
user_id = cursor.lastrowid
|
||||||
|
contact_rows = [(user_id, mode_ids['slack'], username)]
|
||||||
|
if 'email' in user_info:
|
||||||
|
contact_rows.append((user_id, mode_ids['email'], user_info['email']))
|
||||||
|
if 'phone' in user_info:
|
||||||
|
contact_rows.append((user_id, mode_ids['call'], user_info['phone']))
|
||||||
|
contact_rows.append((user_id, mode_ids['sms'], user_info['phone']))
|
||||||
|
if contact_rows:
|
||||||
|
cursor.executemany(
|
||||||
|
'''INSERT INTO `user_contact` (`user_id`, `mode_id`, `destination`)
|
||||||
|
VALUES (%s, %s, %s)''',
|
||||||
|
contact_rows)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
logger.info('Inserted %s users', len(users_to_insert))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_users(connection, users_to_delete):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('UPDATE `user` SET `active` = 0 WHERE `name` IN %s', users_to_delete)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
logger.info('Marked %s users as inactive', len(users_to_delete))
|
||||||
|
|
||||||
|
|
||||||
|
def sync_action(slack_client):
|
||||||
|
re = slack_client.api_call("users.list")
|
||||||
|
if not re.get('ok'):
|
||||||
|
logger.error('Failed to fetch user list from slack')
|
||||||
|
return
|
||||||
|
|
||||||
|
slack_members = re['members']
|
||||||
|
slack_users = {}
|
||||||
|
|
||||||
|
for m in slack_members:
|
||||||
|
if m['name'] == 'slackbot' or m['deleted']:
|
||||||
|
continue
|
||||||
|
user_profile = m['profile']
|
||||||
|
slack_users[m['name']] = {
|
||||||
|
'name': m['name'],
|
||||||
|
'full_name': user_profile['real_name'],
|
||||||
|
'photo_url': user_profile['image_512'],
|
||||||
|
'email': user_profile['email'],
|
||||||
|
}
|
||||||
|
if 'phone' in user_profile:
|
||||||
|
slack_users[m['name']]['phone'] = normalize_phone_number(user_profile['phone'])
|
||||||
|
|
||||||
|
connection = db.connect()
|
||||||
|
# cache mode ids
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('SELECT `id`, `name` FROM `contact_mode')
|
||||||
|
mode_ids = {row[1]: row[0] for row in cursor}
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
slack_usernames = set(slack_users.viewkeys())
|
||||||
|
oncall_usernames = set(fetch_oncall_usernames(connection))
|
||||||
|
|
||||||
|
users_to_insert = slack_usernames - oncall_usernames
|
||||||
|
users_to_delete = oncall_usernames - slack_usernames
|
||||||
|
|
||||||
|
logger.info('users to insert: %s', users_to_insert)
|
||||||
|
logger.info('users to delete: %s', users_to_delete)
|
||||||
|
|
||||||
|
insert_users(connection, slack_users, users_to_insert, mode_ids)
|
||||||
|
delete_users(connection, users_to_delete)
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main(config):
|
||||||
|
slack_config = config.get('slack')
|
||||||
|
if not slack_config:
|
||||||
|
logger.error('slack config not found!')
|
||||||
|
return
|
||||||
|
|
||||||
|
oauth_access_token = slack_config.get('oauth_access_token')
|
||||||
|
if not oauth_access_token:
|
||||||
|
logger.error('slack oauth_access_token not found!')
|
||||||
|
return
|
||||||
|
|
||||||
|
slack_client = SlackClient(oauth_access_token)
|
||||||
|
sync_cycle = config['user_sync'].get('cycle', 60 * 60)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
start = time.time()
|
||||||
|
sync_action(slack_client)
|
||||||
|
duration = time.time() - start
|
||||||
|
logger.info('Slack user sync finished, took %ss, sleep for %ss.',
|
||||||
|
duration, sync_cycle - duration)
|
||||||
|
sleep(sync_cycle - duration)
|
||||||
Reference in New Issue
Block a user