Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoint to create slack channel for a given proposal #76

Merged
merged 30 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c2ce4cb
Add new file slack_service.py
stuartcampbell Feb 7, 2024
ce40684
Merge branch 'NSLS2:main' into slack-integration
stuartcampbell Feb 12, 2024
5494449
Initial work on slack service
stuartcampbell Apr 25, 2024
2c44204
Merge branch 'main' into slack-integration
stuartcampbell May 4, 2024
8facbd8
Admin endpoint to create proposal slack channels
stuartcampbell May 4, 2024
fed534c
Add additional parameters required for slack
stuartcampbell May 4, 2024
7c7e3af
Method to return name of slack channel for a proposal
stuartcampbell May 4, 2024
c0bae1f
Initial work on slack_service
stuartcampbell May 4, 2024
71fe60d
General package upgrades
stuartcampbell May 4, 2024
611a526
Merge branch 'NSLS2:main' into slack-integration
stuartcampbell May 9, 2024
b92a6b1
Add slack_channel_id field to proposal document
stuartcampbell May 10, 2024
7028a6b
Add field to store slack admins
stuartcampbell May 10, 2024
c2e2c20
Make testing prefix allowable by slack
stuartcampbell May 10, 2024
5735262
Add model for SlackBot
stuartcampbell May 10, 2024
68f0a91
Initial version of slack_service
stuartcampbell May 10, 2024
6c7fcf3
Endpoint to create proposal slack channel
stuartcampbell May 10, 2024
3f4977d
feat: Add SlackChannelManagersView model
stuartcampbell May 10, 2024
a87ec15
feat: Add endpoint to retrieve Slack channel managers for a beamline
stuartcampbell May 10, 2024
7906241
Code formating and linting changes
stuartcampbell May 11, 2024
3aeaa86
feat: Add SlackUser model
stuartcampbell May 11, 2024
701568f
feat: Retrieve Slack user IDs of channel managers for a beamline
stuartcampbell May 11, 2024
9a9bf34
feat: Add functionality to add users to a channel.
stuartcampbell May 11, 2024
d2363c0
feat: Add functionality to create Slack channel for proposal
stuartcampbell May 11, 2024
98b8768
Upgrade project dependencies
stuartcampbell May 7, 2024
8129227
Merge branch 'main' into slack-integration
stuartcampbell May 16, 2024
2d6f3f0
Updated create_channel function to enforce privacy setting and remove…
stuartcampbell May 17, 2024
a65318f
Correct field name
stuartcampbell May 17, 2024
1827b70
Update src/nsls2api/api/v1/admin_api.py
stuartcampbell May 17, 2024
5304459
Change default for data_admins be []
stuartcampbell May 17, 2024
b6c931c
Made channel_id and name mandatory fields in response model.
stuartcampbell May 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ requests==2.31.0
# via locust
rich==13.7.1
# via textual
roundrobin==0.0.4
# via locust
ruff==0.4.4
setuptools==69.5.1
# via
Expand Down
82 changes: 80 additions & 2 deletions src/nsls2api/api/v1/admin_api.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from typing import Annotated

import fastapi
from fastapi import Depends, HTTPException, Request
from fastapi import Depends, HTTPException

from nsls2api.infrastructure import config
from nsls2api.infrastructure.logging import logger
from nsls2api.infrastructure.security import (
validate_admin_role,
generate_api_key,
)
from nsls2api.models.apikeys import ApiUser
from nsls2api.services import background_service
from nsls2api.models.slack_models import SlackChannelCreationResponseModel
from nsls2api.services import beamline_service, proposal_service, slack_service

# router = fastapi.APIRouter()
router = fastapi.APIRouter(
Expand Down Expand Up @@ -50,3 +52,79 @@ async def generate_user_apikey(username: str):
return await generate_api_key(username)


@router.post("/admin/slack/create-proposal-channel/{proposal_id}")
async def create_slack_channel(proposal_id: str) -> SlackChannelCreationResponseModel:
proposal = await proposal_service.proposal_by_id(int(proposal_id))

if proposal is None:
return fastapi.responses.JSONResponse(
{"error": f"Proposal {proposal_id} not found"}, status_code=404
)

channel_name = proposal_service.slack_channel_name_for_proposal(proposal_id)

if channel_name is None:
return fastapi.responses.JSONResponse(
{"error": f"Slack channel name cannot be generated for proposal {proposal_id}"},
status_code=404,
)

channel_id = await slack_service.create_channel(
channel_name,
is_private=True,
)

if channel_id is None:
return fastapi.responses.JSONResponse(
{"error": f"Slack channel creation failed for proposal {proposal_id}"},
status_code=500,
)

logger.info(f"Created slack channel '{channel_name}' for proposal {proposal_id}.")

# Store the created slack channel ID
proposal.slack_channel_id = channel_id
await proposal.save()

# Add the beamline slack channel managers to the channel
slack_managers_added = []
for beamline in proposal.instruments:
slack_managers = await beamline_service.slack_channel_managers(beamline)
logger.info(
f"Adding Slack channel managers for {beamline} beamline [{slack_managers}]."
)
if len(slack_managers) > 0:
slack_service.add_users_to_channel(
channel_id=channel_id, user_ids=slack_managers
)
slack_managers_added.append(slack_managers)

# Add the users on the proposal to the channel
stuartcampbell marked this conversation as resolved.
Show resolved Hide resolved
proposal_user_ids = []
for user in proposal.users:
# If username is populated then user has an account
if user.username is not None:
user_slack_id = slack_service.lookup_userid_by_email(user.email)
if user_slack_id is None:
logger.info(f"User {user.username} does not have a slack_id")
else:
logger.info(
f"Adding user {user.username} ({user_slack_id}) to slack channel..."
)
proposal_user_ids.append(user_slack_id)

logger.info(
f"Slack users {proposal_user_ids} will be added to the proposal channel"
)

# TODO: Uncomment to actually add the users when we are sure!!
# slack_service.add_users_to_channel(channel_id=channel_id, user_ids=proposal_user_ids)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line that needs to be uncommented to add all the users to the slack channel


response_model = SlackChannelCreationResponseModel(
channel_id=channel_id,
channel_name=channel_name,
beamline_slack_managers=slack_managers_added,
user_ids=proposal_user_ids,
)

return response_model
8 changes: 8 additions & 0 deletions src/nsls2api/api/v1/beamline_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ async def get_beamline_accounts(name: str, api_key: APIKey = Depends(get_current
)
return service_accounts

@router.get("/beamline/{name}/slack-channel-managers")
async def get_beamline_slack_channel_managers(name: str, api_key: APIKey = Depends(get_current_user)):
slack_channel_managers = await beamline_service.slack_channel_managers(name)
if slack_channel_managers is None:
raise HTTPException(
status_code=404, detail=f"Beamline named {name} could not be found"
)
return slack_channel_managers

@router.get(
"/beamline/{name}/detectors", response_model=DetectorList, include_in_schema=True
Expand Down
5 changes: 2 additions & 3 deletions src/nsls2api/api/v1/proposal_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, Optional
from typing import Annotated
import fastapi
from fastapi import Depends, Query, Request
from fastapi import Depends, Query

from nsls2api.api.models.proposal_model import (
CommissioningProposalsList,
Expand Down Expand Up @@ -223,4 +223,3 @@ async def get_proposal_directories(proposal_id: int) -> ProposalDirectoriesList:
directory_count=len(directories),
)
return response_model

6 changes: 6 additions & 0 deletions src/nsls2api/infrastructure/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class Settings(BaseSettings):
use_socks_proxy: bool = False
socks_proxy: str

# Slack settings
slack_bot_token: str
superadmin_slack_user_token: str
slack_signing_secret: str
nsls2_workspace_team_id: str

model_config = SettingsConfigDict(
env_file=str(Path(__file__).parent.parent / ".env")
)
Expand Down
16 changes: 12 additions & 4 deletions src/nsls2api/models/beamlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ class Settings:
projection = {"data_root": "$custom_root_directory"}


class SlackChannelManagersView(pydantic.BaseModel):
slack_channel_managers: list[str] | None = []
stuartcampbell marked this conversation as resolved.
Show resolved Hide resolved

class Settings:
projection = {"slack_channel_managers": "$slack_channel_managers"}


class EndStation(pydantic.BaseModel):
name: str
service_accounts: Optional[ServiceAccounts] = None
Expand All @@ -128,11 +135,12 @@ class Beamline(beanie.Document):
pass_id: Optional[str]
nsls2_redhat_satellite_location_name: Optional[str]
service_accounts: ServiceAccounts | None = None
endstations: Optional[list[EndStation]]
data_admins: Optional[list[str]]
endstations: Optional[list[EndStation]] = []
slack_channel_managers: Optional[list[str]] = []
data_admins: Optional[list[str]] = []
custom_data_admin_group: Optional[str] = None
github_org: Optional[str]
ups_id: Optional[str]
github_org: Optional[str] = None
ups_id: Optional[str] = None
data_root: Optional[str] = None
services: Optional[list[BeamlineService]] = []
detectors: Optional[list[Detector]] = []
Expand Down
1 change: 1 addition & 0 deletions src/nsls2api/models/proposals.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Proposal(beanie.Document):
cycles: Optional[list[str]] = []
users: Optional[list[User]] = []
safs: Optional[list[SafetyForm]] = []
slack_channel_id: Optional[str] = None
created_on: datetime.datetime = pydantic.Field(
default_factory=datetime.datetime.now
)
Expand Down
21 changes: 21 additions & 0 deletions src/nsls2api/models/slack_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pydantic


class SlackBot(pydantic.BaseModel):
username: str
user_id: str
bot_id: str


class SlackUser(pydantic.BaseModel):
user_id: str
username: str
email: str


class SlackChannelCreationResponseModel(pydantic.BaseModel):
channel_id: str
channel_name: str
beamline_slack_managers: list[str] | None = []
user_ids: list[str] | None = []
message: str | None = None
30 changes: 30 additions & 0 deletions src/nsls2api/services/beamline_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ServicesOnly,
ServiceAccounts,
ServiceAccountsView,
SlackChannelManagersView,
WorkflowServiceAccountView,
IOCServiceAccountView,
EpicsServicesServiceAccountView,
Expand All @@ -23,6 +24,7 @@
DataRootDirectoryView,
LsdcServiceAccountView,
)
from nsls2api.services import slack_service


async def beamline_count() -> int:
Expand Down Expand Up @@ -345,3 +347,31 @@ async def uses_synchweb(name: str) -> bool:
return True
else:
return False


async def slack_channel_managers(beamline_name: str) -> list[str]:
"""
Retrieves the Slack user IDs of the channel managers for a given beamline.

Args:
beamline_name (str): The name of the beamline.

Returns:
list[str]: A list of Slack user IDs of the channel managers.

"""
beamline = await Beamline.find_one(Beamline.name == beamline_name.upper()).project(
SlackChannelManagersView
)
if beamline is None:
return None

slack_ids = []
for user in beamline.slack_channel_managers:
# Staff have to have a BNL email account
email = f"{user}@bnl.gov"
user_id = slack_service.lookup_userid_by_email(email=email)
if user_id:
slack_ids.append(user_id)

return slack_ids
3 changes: 3 additions & 0 deletions src/nsls2api/services/proposal_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ async def fetch_data_sessions_for_username(username: str) -> list[str]:
def generate_data_session_for_proposal(proposal_id: int) -> str:
return f"pass-{str(proposal_id)}"

def slack_channel_name_for_proposal(proposal_id: str) -> str:
#TODO: Actually make this configurable and more sensible
return f"test-sic-{str(proposal_id)}"

async def proposal_by_id(proposal_id: int) -> Optional[Proposal]:
"""
Expand Down
Loading
Loading