diff --git a/conditional/blueprints/housing.py b/conditional/blueprints/housing.py index 0cbc9cdd..ced48ea0 100644 --- a/conditional/blueprints/housing.py +++ b/conditional/blueprints/housing.py @@ -10,8 +10,10 @@ from conditional.util.ldap import ldap_get_current_students from conditional.util.ldap import ldap_get_member from conditional.util.ldap import ldap_get_roomnumber +from conditional.util.ldap import ldap_get_slack_uid from conditional.util.ldap import ldap_is_eval_director from conditional.util.ldap import ldap_set_active +from conditional.util.slack import add_active_usergroup_user, add_meetings_usergroup_user from conditional.util.user_dict import user_dict_is_eval_director logger = structlog.get_logger() @@ -114,7 +116,12 @@ def change_room_numbers(rmnumber, user_dict=None): log.info(f'{occupant} assigned to room {rmnumber}') ldap_set_active(account) log.info(f'{occupant} marked as active because of room assignment') - # Delete any old occupants that are no longer in room. + slack_uid = ldap_get_slack_uid(account) + add_active_usergroup_user(slack_uid) + log.info(f"Add user {user_dict['username']} to @active group") + add_meetings_usergroup_user(slack_uid) + log.info(f"Add user {user_dict['username']} to @meetings group") + # Delete any old occupants that are no longer in room. for old_occupant in [account for account in current_students if ldap_get_roomnumber(account) == str(rmnumber) and account.uid not in update["occupants"]]: diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 7af8d02b..4f4a2e22 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -39,6 +39,7 @@ from conditional.util.ldap import ldap_get_active_members from conditional.util.ldap import ldap_get_member from conditional.util.ldap import ldap_get_current_students +from conditional.util.ldap import ldap_get_slack_uid from conditional.util.ldap import _ldap_add_member_to_group as ldap_add_member_to_group from conditional.util.ldap import _ldap_remove_member_from_group as ldap_remove_member_from_group @@ -47,6 +48,8 @@ from conditional.util.user_dict import user_dict_is_active, user_dict_is_bad_standing, user_dict_is_current_student, \ user_dict_is_eval_director, user_dict_is_financial_director +from conditional.util.slack import add_active_usergroup_user, add_meetings_usergroup_user, purge_active_usergroup, purge_frosh_usergroup, purge_meetings_usergroup + logger = structlog.get_logger() member_management_bp = Blueprint('member_management_bp', __name__) @@ -526,10 +529,17 @@ def member_management_make_user_active(user_dict=None): or user_dict_is_active(user_dict) \ or user_dict_is_bad_standing(user_dict): return "must be current student, not in bad standing and not active", 403 - + ldap_set_active(user_dict['account']) log.info(f"Make user {user_dict['username']} active") + account = user_dict['account'] + slack_uid = ldap_get_slack_uid(account) + add_active_usergroup_user(slack_uid) + log.info(f"Add user {user_dict['username']} to @active group") + add_meetings_usergroup_user(slack_uid) + log.info(f"Add user {user_dict['username']} to @meetings group") + clear_members_cache() return jsonify({"success": True}), 200 @@ -570,6 +580,15 @@ def clear_active_members(user_dict=None): if account.uid != user_dict['username']: log.info(f'Remove {account.uid} from Active Status') ldap_set_inactive(account) + + # Clear the @active, @meetings, and @frosh Slack group + account = user_dict['account'] + slack_uid = ldap_get_slack_uid(account) + purge_active_usergroup(slack_uid) + purge_meetings_usergroup(slack_uid) + purge_frosh_usergroup(slack_uid) + log.info(f"Purged users from @active, @meetings, and @frosh for the new year") + return jsonify({"success": True}), 200 diff --git a/conditional/util/ldap.py b/conditional/util/ldap.py index a34ad086..bcefa58c 100644 --- a/conditional/util/ldap.py +++ b/conditional/util/ldap.py @@ -71,6 +71,13 @@ def ldap_get_roomnumber(account) -> str: except AttributeError: return "" +@service_cache(maxsize=128) +def ldap_get_slack_uid(account) -> str: + try: + return account.get("slackUID")[0] + except KeyError: + return "" + @service_cache(maxsize=128) def ldap_is_active(account) -> bool: return _ldap_is_member_of_group(account, 'active') diff --git a/conditional/util/slack.py b/conditional/util/slack.py new file mode 100644 index 00000000..bbe7eb14 --- /dev/null +++ b/conditional/util/slack.py @@ -0,0 +1,64 @@ +from slack_sdk import WebClient +from slack_sdk.errors import SlackRequestError + +from conditional import app + +client = WebClient(token=app.config['SLACK_APP_TOKEN']) +active_usergroup_id = "S0C7AK4AXEV" # should figure out a better way to do this rather than hardcoding +frosh_usergroup_id = "S0C7CBNRPC4" +meetings_usergroup_id = "S0C78GDRLG2" + +def get_usergroup_users(usergroup_id): + response = client.usergroups_users_list( + usergroup=usergroup_id + ) + + users = response['users'] + response_code = response['ok'] + if response_code == 'false': + raise SlackRequestError(response["error"]) + + return users + +def add_usergroup_user(usergroup_id, slack_uid): + group_users = get_usergroup_users(usergroup_id) + group_users.append(slack_uid) + + response = client.usergroups_users_update( + usergroup=usergroup_id, + users=group_users + ) + + response_code = response["ok"] + if response_code == 'false': + raise SlackRequestError(response["error"]) + + return response_code + +def add_active_usergroup_user(slack_uid): + return add_usergroup_user(active_usergroup_id, slack_uid) + +def add_meetings_usergroup_user(slack_uid): + return add_usergroup_user(meetings_usergroup_id, slack_uid) + +def purge_usergroup_users(usergroup_id, slack_uid): + response = client.usergroups_users_update( + usergroup=usergroup_id, + users=slack_uid + ) + + response_code = response["ok"] + if response_code == 'false': + raise SlackRequestError(response["error"]) + + return response_code + +def purge_active_usergroup(slack_uid): + return purge_usergroup_users(active_usergroup_id, slack_uid) + +def purge_frosh_usergroup(slack_uid): + return purge_usergroup_users(frosh_usergroup_id, slack_uid) + +def purge_meetings_usergroup(slack_uid): + return purge_usergroup_users(meetings_usergroup_id, slack_uid) + \ No newline at end of file diff --git a/config.env.py b/config.env.py index 1142a2c8..34cde152 100644 --- a/config.env.py +++ b/config.env.py @@ -17,6 +17,7 @@ WEBHOOK_URL = env.get("CONDITIONAL_WEBHOOK_URL", "INSERT URL HERE") DEV_DISABLE_SLACK_PING = env.get("DEV_DISABLE_SLACK_PING", "false") == "true" PROFILING = env.get("CONDITIONAL_PROFILING", "false").lower() == "true" +SLACK_APP_TOKEN = env.get("CONDITIONAL_SLACK_APP_TOKEN", "INSERT TOKEN HERE") # DB Info SQLALCHEMY_DATABASE_URI = env.get("SQLALCHEMY_DATABASE_URI", "postgresql://conditional:fancypantspassword@conditional-postgres:5432/conditional") diff --git a/requirements.in b/requirements.in index 9abff33b..afa76de4 100644 --- a/requirements.in +++ b/requirements.in @@ -23,6 +23,7 @@ pip-tools~=7.5.3 psycopg2-binary~=2.9.3 pylint~=3.3.6 python-editor~=1.0.3 +slack-sdk~=3.45.0 sentry-sdk[flask]~=2.63.0 six~=1.17.0 SQLAlchemy~=2.0.48 diff --git a/requirements.txt b/requirements.txt index 739840c4..715216a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements.in +# +# This file is autogenerated by pip-compile with Python 3.14 +# by the following command: +# +# pip-compile requirements.in +# alembic==1.18.5 # via # -r requirements.in @@ -175,6 +179,8 @@ six==1.17.0 # -r requirements.in # pyjwkest # python-dateutil +slack-sdk==3.45.0 + # via -r requirements.in sqlalchemy==2.0.54 # via # -r requirements.in @@ -210,3 +216,7 @@ wheel==0.48.0 # via pip-tools wrapt==2.1.2 # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools