Skip to content

Commit

Permalink
Merge pull request #76 from stuartcampbell/slack-integration
Browse files Browse the repository at this point in the history
Add endpoint to create slack channel for a given proposal
  • Loading branch information
padraic-shafer authored May 18, 2024
2 parents 266596e + b6c931c commit c118bcf
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 9 deletions.
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
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)

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 = []

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 @@ -352,3 +354,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

0 comments on commit c118bcf

Please sign in to comment.