Skip to content

Commit

Permalink
open source push 3-11-2021
Browse files Browse the repository at this point in the history
  • Loading branch information
noxasaxon committed Mar 12, 2021
1 parent df9c7eb commit 839020f
Show file tree
Hide file tree
Showing 22 changed files with 3,222 additions and 3,912 deletions.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.


END OF TERMS AND CONDITIONS
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,85 @@ If your Socless deployment is successful, you will see a URL that ends in `/slac


Your Slack Bot is now completely configured for use within Socless

## Slack App Permission Scopes

### Bot Token Scopes
Scopes that govern what your app can access.

- app_mentions:read
- calls:read
- calls:write
- channels:history
- channels:join
- channels:manage
- channels:read
- chat:write
- chat:write.customize
- chat:write.public
- commands
- dnd:read
- emoji:read
- files:read
- files:write
- groups:history
- groups:read
- groups:write
- im:history
- im:read
- im:write
- incoming-webhook
- links:read
- links:write
- mpim:history
- mpim:read
- mpim:write
- pins:read
- pins:write
- reactions:read
- reactions:write
- reminders:read
- reminders:write
- remote_files:read
- remote_files:share
- remote_files:write
- team:read
- usergroups:read
- usergroups:write
- users.profile:read
- users:read
- users:read.email
- users:write
- workflow.steps:execute

### User Token Scopes
Scopes that access user data and act on behalf of users that authorize them.

- channels:history
- channels:read
- channels:write
- chat:write
- emoji:read
- files:read
- groups:history
- groups:read
- groups:write
- identify
- im:history
- im:read
- im:write
- links:read
- links:write
- mpim:history
- mpim:read
- mpim:write
- pins:read
- pins:write
- reactions:read
- reactions:write
- search:read
- usergroups:read
- users.profile:read
- users:read
- users:read.email
- users:write
136 changes: 109 additions & 27 deletions common_files/slack_helpers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import slack
import os
import boto3

CACHE_USERS_TABLE = os.environ.get("CACHE_USERS_TABLE")
SOCLESS_BOT_TOKEN = os.environ["SOCLESS_BOT_TOKEN"]
SOCLESS_USER_TOKEN = os.environ["SOCLESS_USER_TOKEN"]

slack_client = slack.WebClient(SOCLESS_BOT_TOKEN)
slack_user_client = slack.WebClient(SOCLESS_USER_TOKEN)


def find_user(name, page_limit=1000, include_locale="false"):
class SlackError(Exception):
pass


def find_user(name: str, page_limit=1000, include_locale="false"):
"""Find a user's Slack profile based on their full or display name.
Args:
name: A user's Full Name or Display Name
"""
name_lower = name.lower()
paginate = True
Expand Down Expand Up @@ -40,43 +46,66 @@ def find_user(name, page_limit=1000, include_locale="false"):
return {"found": False}


def get_profile_via_id(slack_id):
"""Fetch user's slack profile with their slack_id.
def get_slack_id_from_username(username: str):
"""Fetch user's slack_id from their username.
Checks against the dynamoDB cache (if enabled), or paginates through slack API users.list
looking for the supplied username. If cache enabled, saves the found slack_id
Args:
slack_id : (string) slack user id ex. W1234567
username : (string) slack username (usually display_name)
Returns:
slack_id
"""
resp = slack_user_client.users_profile_get(user=slack_id)
return resp["profile"]
slack_id = get_id_from_cache(username) if CACHE_USERS_TABLE else ""

if not slack_id:
search = find_user(username)
if not search["found"]:
raise Exception(f"Unable to find user: {username}")

def get_channel_id(channel_name, channel_type):
slack_id = search["user"]["id"]
if CACHE_USERS_TABLE:
save_user_to_cache(username=username, slack_id=slack_id)

return slack_id


def get_user_info_via_id(slack_id):
"""
API Docs https://api.slack.com/methods/users.info
"""
Fetches the ID of a Slack Channel or User
resp = slack_client.users_info(user=slack_id)
return resp["user"]


def resolve_slack_target(target_name: str, target_type: str) -> str:
"""Fetches the ID of a Slack Channel or User.
Args:
channel_name: (string) The name of the channel / name of user / slack id of channel/user
channel_type: (string) The Channel type, either "user" or "channel" or "slack_id"
target_name: (string) The name of the channel / name of user / slack id of channel/user
target_type: (string) The Channel type, either "user" or "channel" or "slack_id"
Returns:
(string) A Slack ID that can be used to message the channel directly
"""
if channel_type == "slack_id":
channel_id = channel_name
elif channel_type == "user":
user = find_user(channel_name)
channel_id = user["user"]["id"] if user["found"] else False
if not channel_id:
raise Exception(f"Unable to find user: {channel_name}")

if target_type == "slack_id":
slack_id = target_name
elif target_type == "user":
slack_id = get_slack_id_from_username(target_name)
elif target_type == "channel":
slack_id = target_name if target_name.startswith("#") else f"#{target_name}"
else:
channel_id = f"#{channel_name}"
raise Exception(
f"target_type is not 'channel|user|slack_id'. failed target_type: {target_type} for target: {target_name}"
)

return channel_id
return slack_id


def paginated_api_call(api_method, response_objects_name, **kwargs):
"""
Calls api method and cycles through all pages to get all objects
:param method: api method to call
:param response_objects_name: name of collection in response json
:param kwargs: url params to pass to call, additionally to limit and cursor which will be added automatically
"""Calls api method and cycles through all pages to get all objects.
Args:
api_method: api method to call
response_objects_name: name of collection in response json
kwargs: url params to pass to call, additionally to limit and cursor which will be added automatically
"""

ret = list()
Expand All @@ -90,11 +119,64 @@ def paginated_api_call(api_method, response_objects_name, **kwargs):
response_objects = r.get(response_objects_name)
if response_objects is not None:
for channel in r[response_objects_name]:
channel_name = channel.get("name")
if isinstance(channel, str):
channel_name = channel
else:
channel_name = channel.get("name")

ret.append(channel_name)
metadata = r.get("response_metadata")
if metadata is not None:
cursor = metadata["next_cursor"]
else:
cursor = ""

return ret


def slack_post_msg_wrapper(target, target_type, **kwargs):
target_id = resolve_slack_target(target, target_type)
resp = slack_client.chat_postMessage(channel=target_id, **kwargs)

if not resp.data["ok"]:
raise SlackError(
f"Slack error during post_message to {target}: {resp.data['error']}"
)
print(f'returned channel: {resp["channel"]}')
return resp


def get_id_from_cache(username: str) -> str:
"""Check if username exists in cache, return their slack_id.
Args:
username: slack username
Returns:
slack_id
"""
dynamodb = boto3.resource("dynamodb")
if not CACHE_USERS_TABLE:
raise Exception(
"env var CACHE_USERS_TABLE is not set, please check socless-slack serverless.yml"
)
table_resource = dynamodb.Table(CACHE_USERS_TABLE)
key_obj = {"username": username}
response = table_resource.get_item(TableName=CACHE_USERS_TABLE, Key=key_obj)

return response["Item"]["slack_id"] if "Item" in response else False


def save_user_to_cache(username: str, slack_id: str):
"""Save a username -> slack_id mapping to the cache table
Args:
username: slack username
slack_id: user's slack id
"""
dynamodb = boto3.resource("dynamodb")
if not CACHE_USERS_TABLE:
raise Exception(
"env var CACHE_USERS_TABLE is not set, please check socless-slack serverless.yml"
)
table_resource = dynamodb.Table(CACHE_USERS_TABLE)
new_item = {"username": username, "slack_id": slack_id}
response = table_resource.put_item(TableName=CACHE_USERS_TABLE, Item=new_item)
print(response)
33 changes: 18 additions & 15 deletions functions/check_user_in_channel/lambda_function.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
from socless import *
from slack_helpers import slack_client, find_user, get_channel_id, paginated_api_call
from socless import socless_bootstrap
from slack_helpers import slack_client, resolve_slack_target, paginated_api_call


def handle_state(user_id, target_channel_id):
"""
Check if user is in certain slack channel
:param user_id: user id of the user invoking the slash command
:param target_channel_id: the channel id to be checked if user is in
:return boolean value indicating whether the user is in the channel
def handle_state(user_id: str, target_channel_id: str):
"""Check if user is in a particular slack channel.
Args:
user_id: user id of the user invoking the slash command
target_channel_id: the channel id to be checked if user is in
Returns:
ok: (bool) True if user is found in the channel
"""

ret = paginated_api_call(slack_client.conversations_members,
'members',
target_channel_id
)
if user_id not in ret['members']:
return {"ok": False}
else:
response = paginated_api_call(
api_method=slack_client.conversations_members,
response_objects_name="members",
channel=target_channel_id,
)

if user_id in response:
return {"ok": True}

return {"ok": False}


def lambda_handler(event, context):
return socless_bootstrap(event, context, handle_state)
69 changes: 35 additions & 34 deletions functions/create_channel/lambda_function.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
from socless import *
from socless import socless_bootstrap
import os
import slack
from slack_helpers import slack_client


def handle_state(channel_name, is_private):
def handle_state(channel_name, user_ids=[], is_private=False):
"""Create slack channel and invite any provided slack_ids.
Args:
channel_name (str): The name of channel to be created.
user_ids (list) : slack_ids of users invited to new channel. Private Channels require a user.
is_private (boolean): if the channel is private.
Token_Type: xoxp (slack legacy user token)
Note:
- See https://api.slack.com/methods/conversations.create for more details on how to create private channel
"""
Create slack channel
Args:
channel_name (str): The name of channel to be created.
is_private (boolean): if the channel private
Token_Type: xoxp
Note:
- See https://api.slack.com/methods/conversations.create for more details on how to create private channel
"""

SOCLESS_USER_TOKEN = os.environ['SOCLESS_USER_TOKEN']
slack_api_client = slack.WebClient(SOCLESS_USER_TOKEN)

try:
res = slack_api_client.conversations_create(
name=channel_name,
is_private=is_private

if isinstance(user_ids, str):
user_ids = user_ids.split(",")

create_response = slack_client.conversations_create(
name=channel_name, is_private=is_private, user_ids=user_ids
)

created_channel_id = create_response["channel"]["id"]
bot_user_id = create_response["channel"]["creator"]

if user_ids:
user_ids = [x.strip() for x in user_ids if x != bot_user_id]

invite_response = slack_client.conversations_invite(
channel=created_channel_id, users=user_ids
)
created_channel_id = res["channel"]['id']
return {
"ok": True,
"created_channel_id": created_channel_id,
"channel_name": channel_name
}
except Exception as e:
s = str(e)
err_msg = s.split("'detail': ", 1)[1]
err_msg = err_msg[:len(err_msg) - 1]
return {
"ok": False,
"error": err_msg
}

return {
"ok": True,
"created_channel_id": created_channel_id,
"channel_name": channel_name,
"added_users": user_ids,
}


def lambda_handler(event, context):
return socless_bootstrap(event, context, handle_state)
return socless_bootstrap(event, context, handle_state)
Loading

0 comments on commit 839020f

Please # to comment.