1
0
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:
Qingping Hou
2017-05-20 10:25:20 -07:00
committed by Daniel Wang
parent fc12d9b671
commit 8a54b06195
8 changed files with 196 additions and 10 deletions

View File

@@ -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'

View File

@@ -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',

View File

@@ -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'

View 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()

View File

@@ -20,6 +20,7 @@ var oncall = {
modes: [ modes: [
'email', 'email',
'sms', 'sms',
'slack',
'call' 'call'
], ],
timezones: [ timezones: [

View File

@@ -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>

View File

View 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)