I have this issue when run the script in the third time:
Traceback (most recent call last): File "D:\Git\Slack_Jira_Sync\sync.py", line 12, in <module> import requests ModuleNotFoundError: No module named 'requests'
Hello everyone, I have a question about the Python script, do I use it on a server so that it bridges the gap between Jira and Slack? Do I have to run it with a specific frequency?
# Set base log level log.basicConfig(level=log.INFO)
# Pagination limit for Slack 'list_users' SLACK_PAGINATION_LIMIT = 30
# Timeout for API calls in seconds API_TIMEOUT = 60
# Maximum number of retry attempts MAX_RETRY_ATTEMPTS = 5 # Base delay in milliseconds between API calls BASE_API_DELAY_MS = 1000 # Increased delay # Exponential backoff factor BACKOFF_FACTOR = 2
# Slack API rate limit status codes SLACK_RATE_LIMIT_STATUS_CODES = {429, 429}
while retry_attempts < MAX_RETRY_ATTEMPTS: try: return func(*args, **kwargs) except requests.exceptions.HTTPError as e: if e.response.status_code in SLACK_RATE_LIMIT_STATUS_CODES: # Rate limit exceeded retry_attempts += 1 log.warning( f"Rate limit exceeded. Retrying after {base_delay}ms (Attempt {retry_attempts})..." ) time.sleep(base_delay / 1000.0) # Delay before retry # Apply exponential backoff with a random jitter factor base_delay *= (BACKOFF_FACTOR + random.uniform(0, 1)) # Check if rate limit is near, apply longer delay if necessary if 'Retry-After' in e.response.headers: retry_after = int(e.response.headers['Retry-After']) if retry_after > base_delay: log.warning( f"Rate limit near. Applying longer delay ({retry_after}s)" ) time.sleep(retry_after) else: raise e # Propagate other HTTP errors except requests.exceptions.RequestException as e: log.error("Request error:", e) raise e # Propagate other request errors
log.error("Exceeded maximum retry attempts. Aborting.") sys.exit(1)
return wrapper
def gather_slack_details(client): """Returns a dict of all users (not bots, with email IDs) for a given workspace (as authenticated by SLACK_BOT_TOKEN)
Args: client: slack.web.client.WebClient Slack WebClient module which is already authenticated Returns: dict Returns a dict, where the keys are 'emails' and values are tuples of Slack usernames (which can be tagged with `@`) and Slack IDs """
# Helper method to call a page of users @exponential_backoff def _get_active_users(client, next_cursor=None): log.debug("Getting next {} members...".format(SLACK_PAGINATION_LIMIT)) response = client.users_list(limit=SLACK_PAGINATION_LIMIT, cursor=next_cursor) users = response["members"] response_metadata = response["response_metadata"]
# Format email:slack_id object if user is active and has an email return {u['profile']['email']: (u['name'], u['id']) for u in users if not u['is_bot'] and not u['deleted'] and 'email' in u['profile']}, response_metadata['next_cursor']
# Check if next page available, and update users while next_cursor: next_active_users, next_cursor = _get_active_users(client, next_cursor) active_users.update(next_active_users) return active_users
def __init__(self, server_url, username, api_token): """Create a new JIRA Webclient
Args: server_url: str JIRA server side URL username: str Atlassian account email (`@atlassian.com`) associated with JIRA server api_token: str User API token generated from `https://id.atlassian.com/manage/api-tokens` """ self.server_url = server_url self.username = username self.api_token = api_token self.search_url = parse.urljoin(self.server_url, self._SEARCH_PATH) self.property_url = parse.urljoin(self.server_url, self._PROPERTY_PATH)
@exponential_backoff def get_user(self, email): """Search for a user by e-mail and return the account ID if found
Args: email: str Email ID pattern to search user with
Returns: str/None Return the account ID of the user if found in JIRA server, else return None
""" response = requests.get(self.search_url, auth=(self.username, self.api_token), headers=self._HEADERS, params={'query': email, 'maxResults': 1}, timeout=API_TIMEOUT) if response: json_response = response.json() if len(json_response) > 0: return json_response[0]['accountId'] log.warning("No user found for email: {}".format(email)) else: self._wrap_error("Failed to call search API.", response)
@exponential_backoff def set_user_property(self, account_id, slack_username, slack_id, key='metadata'): """Set user property (slack_id) for a given account ID in the JIRA server
Args: account_id: str Account ID of the user slack_username: str Slack username (which can be tagged) slack_id: str Slack ID of the user key: str (Optional, defaults to 'metadata') Path where the user property is updated
Returns: bool Returns True on successful property update, False otherwise
""" response = requests.put(parse.urljoin(self.property_url, key), auth=(self.username, self.api_token), headers=self._HEADERS, params={'accountId': account_id}, data=json.dumps({'slack_username': slack_username, 'slack_id': slack_id}), timeout=API_TIMEOUT) if not response: self._wrap_error("Could not add property for account: {}".format(account_id), response) return False else: log.info("User property `metadata.value.slack_id:{}, metadata.value.slack_username:{}` set for {}".format( slack_id, slack_username, account_id)) return True
def get_slack_info(self, account_id): """Get slack username, slack ID property values (under {account_id}/properties/metadata) if set for user
Args: account_id: str Account ID of the user
Returns: tuple(str, str) (Taggable Slack username of the user, Slack ID) if found
""" response = requests.get(parse.urljoin(self.property_url, 'metadata'), auth=(self.username, self.api_token), headers=self._HEADERS, params={'accountId': account_id}, timeout=API_TIMEOUT) if response: json_response = response.json() metadata_value = json_response['value'] if 'slack_id' in metadata_value and 'slack_username' in metadata_value: return metadata_value['slack_id'], metadata_value['slack_username'] self._wrap_error("Could not find a slack_id/slack_username property for account: {}".format(account_id), response)
# Gather list of slack users associated with slack token's workspace slack_client = WebClient(token=args.slack_token) slack_users = gather_slack_details(slack_client) total_users = len(slack_users) if not slack_users: log.warning("No user found for the given Slack workspace associated with token. Exiting!") sys.exit(0)
# Create a new JIRA Web Client jira = JIRA(args.jira_url, args.username, args.apikey)
log.info("Looking up {} users in Jira server: {}".format(total_users, args.jira_url)) properties_updated = 0 accounts_found = 0
for email, slack_info in slack_users.items(): # Search user by email slack_username, slack_id = slack_info account_id = jira.get_user(email) if account_id: accounts_found += 1 log.info("Found account ID: {} for user: {}".format(account_id, email)) # Update user property if jira.set_user_property(account_id, slack_username, slack_id): properties_updated += 1
log.info("Finished updating {} properties in {} accounts found in Jira (Total {} Slack members)".format( properties_updated, accounts_found, total_users))
@Kirsten Miller For me this code worked. May be a copy paste issue. Copy the code in Chatgpt and ask for syntax correction. You'll get the corrected code.
@Kirsten Miller probably ChatGPT give you wrong corrected code. Also, code above obviously wrong pasted so it can't work. You can use the code from the start of the article or try to finding around what wrong with your code)
I was able to run the script successfully and read all Slack member IDs. I created the legacy webhook however, it is able to mention only my ID and not all members of my workspace.
I am able to send personal message only to my ID. Any idea on this.
This is working great for me however i don't seem to be able to DM. The override doesn't appear to work. i can @mention reporter and assignee no problem in a message to the WEbhook URL specified channel but if i attempt to override using my Slack Member ID for example it still sends to channel. anyone else seen this behaviour with any tips to solve?
@Sean Button Try using something like this. Work fine for me. But there is limitation to one user for for rule action. If you need more - I didn't found straight solution. My personal workaround - use several same rule action for every user that I need to send message to.
As it turns out everything is fine from a sync perspective. i'm working with Atlassian support at the moment to root cause the issue and possible solution.
So my colleague asked me if we could do the sync that Daniel describes and I was like:
Ugh. I have to figure out where I'm going to run a Python script? And do we schedule it daily? Weekly? And does it just overwrite all user properties? Can it just sync the "new" users?
Then I thought:
Wait, can't we just lookup a specific Slack User ID with a web request when we need it?
@Dzanan Gvozden Hello! If you want to get data from your issues field, you don't need to use .properties.metadata.slack_id part. {{issue.fields.customfield_110087}} probably will work. If you have channel id in this field of course
Probably, it's not possible. Assignee field - it's user instans, but I don't know what's lie inside custom user picker field. Maybe you need to add few more steps to your path to slack_id. Use logging, it'll help you understand what variable you need to call
@Daniel Eads I'm trying to run this script and encountering a similar issue to some others here where I'm getting:
INFO:root:Looking up 68 users in Jira server: https://{domain} WARNING:root:No user found for email: {email 1} WARNING:root:No user found for email: {email 2}
....
INFO:root:Finished updating 0 propertie(s) in 0 account(s) found in Jira (Total 68 Slack member(s))
Not sure why this is happening, as my account (the GUID being used for --username) has all admin access to this Jira instance.
Also, I'm not sure why the script is only identifying 68 total users as there are far more than that in the Jira instance. Is this querying the Slack domain first? We have a lot of guests in Slack who also need to use this integration, how do we support that?
100 comments