Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Mention someone directly in Slack via Jira Automation

100 comments

JOY March 9, 2024

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'

Update: I installed request and it works now.

Gustavo Gabriel Riva
Contributor
March 22, 2024

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?

Karishma Walia April 12, 2024

I am getting this issue:

The server responded with: {'ok': False, 'error': 'missing_scope', 'needed': 'users:read', 'provided': 'incoming-webhook'} 

I checked the permissions and my token as user:read permission. What could be the issue?  

Karishma Walia April 12, 2024

Hi @armcaelria 

I am getting this issue while running the script:

The server responded with: {'ok': False, 'error': 'missing_scope', 'needed': 'users:read', 'provided': 'incoming-webhook'} 

I checked the permissions and my token as user:read permission. What could be the issue?

 

Update: I reinstalled the app and permissions and it worked.

Karishma Walia April 12, 2024

Here is the correct Sync.py script if you are facing "ratelimited" error:

import argparse
import json
import logging as log
import sys
from urllib import parse
import time
import requests
import random

from slack import WebClient

##############################################################################
# Configuration
##############################################################################

# 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}

##############################################################################


def exponential_backoff(func):
"""Decorator to add exponential backoff retry mechanism for API calls
"""

def wrapper(*args, **kwargs):
retry_attempts = 0
base_delay = BASE_API_DELAY_MS

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

active_users, next_cursor = _get_active_users(client)

# 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


class JIRA(object):
_SEARCH_PATH = "rest/api/3/user/search"
_PROPERTY_PATH = "rest/api/3/user/properties/"
_HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json"
}

def _wrap_error(self, error_message, response):
"""Helper to log HTTPResponse errors
"""
log.error("{} [{}] {}".format(error_message, response.status_code, response.json()))

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)


if __name__ == "__main__":
# Parse CLI arguments
parser = argparse.ArgumentParser()
parser.add_argument("--jira_url", type=str, required=True, help="JIRA Server URL")
parser.add_argument("--slack_token", type=str, required=True, help="Slack App Authentication Token")
parser.add_argument("--username", type=str, required=True, help="Atlassian e-mail ID")
parser.add_argument("--apikey", type=str, required=True, help="Atlassian API key")
args, _ = parser.parse_known_args()

# 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 May 2, 2024

@Karishma Walia I am getting an error with the syntax of that - any updates?

 

 File "/Users/coopersmill/sync.py", line 73

    except requests.exceptions.RequestException as e:

    ^^^^^^

SyntaxError: invalid syntax

 

Karishma Walia May 4, 2024

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

armcaelria
Contributor
May 6, 2024

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

Karishma Walia May 14, 2024

@Daniel Eads Hi, 

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.

Thanks

Sean Button July 4, 2024

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?

 

 

armcaelria
Contributor
July 8, 2024

@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.Monosnap Rule builder.png

 

Edited. You need to use user ID from slack

Like Sean Button likes this
권동호 July 10, 2024

ssaas.jpg

When you run the script, you will see something like the image above.

Any idea what's wrong?
It used to work fine, but it doesn't work after a long time.

armcaelria
Contributor
July 10, 2024

@권동호  The request library doesn't install on your machine. Just google "python requests", it's literally first link in google to pypi.org

Like # people like this
Sean Button July 10, 2024

@armcaelria , Thanks for your reply, 

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. 

it appears to original method for configuring the Slack Message is deprecated and as such the updated method is not supporting DM.  - https://community.atlassian.com/t5/Jira-questions/Slack-incoming-webhook-will-be-deprecated-and-not-recommended-to/qaq-p/1826161 

Lets see how it progresses

 

Darryl Lee
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 12, 2024

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?

Why yes, yes we can:

Thank goodness. Because I kind of suck at Python. :-}

This doesn't require knowing or even installing it.

Like Becky likes this
Sean Button September 11, 2024

@Darryl Lee , is there any impact to the amount of API calls that can be made for each platform?

 

ah looks like you answered in the article. :)

Dzanan Gvozden November 7, 2024

How can I handle a custom user field?

For example I have a field called QA Assignee, but when I want to send a slack message to 

@{{QA Assignee.properties.metadata.slack_id}}

It says Error from Slack [404]: channel_not_found 

Anyone had a similar issue?

I also tried:

@{{customfield_110087.properties.metadata.slack_id}}

@{{issue.customfield_110087.properties.metadata.slack_id}}

@{{issue.QA Assignee.properties.metadata.slack_id}}

The results were the same.

armcaelria
Contributor
November 7, 2024

@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

Dzanan Gvozden November 7, 2024

@armcaelria not sure I follow, I need the slack_id for QA assignee as well.

Basically I have 5 tickets which have no time logged on them but these tickets have assignees and QA assignees.

I want to send a slack message to each of these with their corresponding tickets.

So for assignees I send the message with ticket information to 

@{{assignee.properties.metadata.slack_id}}

and this works fine.

 

I want to do the same for QA Assignee with 

@{{QA Assignee.properties.metadata.slack_id}}

but this doesn't work.

Matas Gasparavičius
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
November 7, 2024

Hey @Dzanan Gvozden

Maybe it's multi-user field and has more than 1 user assigned to it?

armcaelria
Contributor
November 7, 2024

@Dzanan Gvozden Try log this field and check what's in it. Probably, you will need to log every step from 

{{QA Assignee.properties.metadata.slack_id}}

Inside automation add audit log action and add to it this field. Otherwise, you can check this fields via API

armcaelria
Contributor
November 7, 2024

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

Dzanan Gvozden November 7, 2024

@Matas Gasparavičius @Arm  it's single user field and I logged it it returns empty

armcaelria
Contributor
November 7, 2024

@Dzanan Gvozden Try logging previous steps from this path

{{QA Assignee.properties.metadata.slack_id}}

Like log

{{QA Assignee.properties.metadata}}

Then 

{{QA Assignee.properties}}

And then 

{{QA Assignee}}

 

Ryan Belmont November 19, 2024

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


Thanks,

Ryan

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events