├── birthday-slackbot ├── tests │ ├── __init__.py │ └── test_app.py ├── chalicelib │ ├── __init__.py │ ├── requirements.txt │ ├── utils.py │ └── upstash.py ├── .gitignore ├── requirements-dev.txt └── app.py ├── LICENSE └── README.md /birthday-slackbot/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /birthday-slackbot/chalicelib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /birthday-slackbot/chalicelib/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /birthday-slackbot/.gitignore: -------------------------------------------------------------------------------- 1 | .chalice 2 | __pycache__ 3 | envSetter.txt -------------------------------------------------------------------------------- /birthday-slackbot/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | chalice==1.26.6 2 | py.test 3 | -------------------------------------------------------------------------------- /birthday-slackbot/tests/test_app.py: -------------------------------------------------------------------------------- 1 | from chalice.test import Client 2 | from app import app 3 | 4 | 5 | def test_index(): 6 | with Client(app) as client: 7 | response = client.http.get('/') 8 | assert response.json_body == {'hello': 'world'} 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 burak-upstash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /birthday-slackbot/chalicelib/utils.py: -------------------------------------------------------------------------------- 1 | from urllib import request 2 | import urllib 3 | from urllib.parse import parse_qsl 4 | import json 5 | import os 6 | import hmac 7 | import hashlib 8 | from datetime import date 9 | 10 | 11 | SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN") 12 | SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET") 13 | 14 | # Returns real name of the slack user. 15 | def getRealName(slackUsers, username): 16 | for user in slackUsers: 17 | if user[0] == username: 18 | return user[2] 19 | return "Nameless" 20 | 21 | # Returns all slack users in the workspace. 22 | def allSlackUsers(): 23 | resultDict = sendPostRequest("https://slack.com/api/users.list", SLACK_BOT_TOKEN) 24 | members = resultDict['members'] 25 | 26 | userMembers = [] 27 | for member in members: 28 | if not member['deleted'] and not member['is_bot']: 29 | userMembers.append([member['name'], member['id'], member['real_name']]) 30 | 31 | return userMembers 32 | 33 | # Returns the id of the given channel. 34 | def channelNameToId(channelName) : 35 | resultDict = sendPostRequest("https://slack.com/api/conversations.list", SLACK_BOT_TOKEN) 36 | for channel in resultDict['channels']: 37 | if (channel['name'] == channelName): 38 | return channel['id'] 39 | return None 40 | 41 | # Posts to given slack channelId with given message. 42 | def postToSlack(channelId, messageText): 43 | data = { 44 | "channel": channelId, 45 | "text": messageText 46 | } 47 | data = json.dumps(data) 48 | data = str(data) 49 | data = data.encode('utf-8') 50 | resultDict = sendPostRequest("https://slack.com/api/chat.postMessage", SLACK_BOT_TOKEN, data) 51 | return resultDict 52 | 53 | # Posts to a slack channel. 54 | def postToChannel(channel, messageText): 55 | channelId = channelNameToId(channel) 56 | return postToSlack(channelId, messageText) 57 | 58 | # Sends a private message to a user with userId. 59 | def sendDm(userId, messageText): 60 | return postToSlack(userId, messageText) 61 | 62 | # Sends generic post request and returns the result. 63 | def sendPostRequest(requestURL, bearerToken, data={}): 64 | req = request.Request(requestURL, method="POST", data=data) 65 | req.add_header("Authorization", "Bearer {}".format(bearerToken)) 66 | req.add_header("Content-Type", "application/json; charset=utf-8") 67 | 68 | r = request.urlopen(req) 69 | resultDict = json.loads(r.read().decode()) 70 | return resultDict 71 | 72 | # Parses and converts the res to dict. 73 | def responseToDict(res): 74 | return dict(parse_qsl(res.decode())) 75 | 76 | 77 | # Dates are given as: YYYY-MM-DD 78 | # Returns difference between current day and the anniversary. 79 | def diffWithTodayFromString(dateString): 80 | now = date.today() 81 | currentYear = now.year 82 | 83 | dateTokens = dateString.split("-") 84 | month = int(dateTokens[1]) 85 | day = int(dateTokens[2]) 86 | 87 | if now > date(currentYear, month, day): 88 | return (date((currentYear + 1), month, day) - now).days 89 | return (date(currentYear, month, day) - now).days 90 | 91 | 92 | # Dates are given as: YYYY-MM-DD 93 | # Calculates the total time that has passed until current date. 94 | def totalTimefromString(dateString): 95 | now = date.today() 96 | 97 | dateTokens = dateString.split("-") 98 | year = int(dateTokens[0]) 99 | month = int(dateTokens[1]) 100 | day = int(dateTokens[2]) 101 | 102 | then = date(year, month, day) 103 | 104 | years = now.year - then.year 105 | return years + 1 106 | 107 | # Validate requests coming to endpoint. 108 | # Hashes request body with timestamp and signing secret. 109 | # Then, compares that hash with slack signature. 110 | def validateRequest(header, body): 111 | 112 | bodyAsString = urllib.parse.urlencode(body) 113 | 114 | timestamp = header['x-slack-request-timestamp'] 115 | slackSignature = header['x-slack-signature'] 116 | baseString = "v0:{}:{}".format(timestamp, bodyAsString) 117 | 118 | h = hmac.new(SLACK_SIGNING_SECRET.encode(), baseString.encode(), hashlib.sha256) 119 | hashResult = h.hexdigest() 120 | mySignature = "v0=" + hashResult 121 | 122 | return mySignature == slackSignature 123 | 124 | # Converts given name to mention string. 125 | def convertToCorrectMention(name): 126 | if name == "channel" or name == "here" or name == "everyone": 127 | return "".format(name) 128 | else: 129 | return "<@{}>".format(name) -------------------------------------------------------------------------------- /birthday-slackbot/chalicelib/upstash.py: -------------------------------------------------------------------------------- 1 | from chalicelib.utils import sendPostRequest, getRealName, allSlackUsers, diffWithTodayFromString, totalTimefromString 2 | import os 3 | 4 | UPSTASH_REST_URL = os.getenv("UPSTASH_REST_URL") 5 | UPSTASH_TOKEN = os.getenv("UPSTASH_TOKEN") 6 | 7 | # Posts to Upstash Rest Url with parameters given. 8 | def postToUpstash(parameters): 9 | requestURL = UPSTASH_REST_URL 10 | for parameter in parameters: 11 | requestURL += ("/" + parameter) 12 | 13 | resultDict = sendPostRequest(requestURL, UPSTASH_TOKEN) 14 | return resultDict['result'] 15 | 16 | 17 | # Sets key-value pair for the event with given parameters. 18 | def setEvent(parameterArray): 19 | 20 | postQueryParameters = ['SET'] 21 | 22 | for parameter in parameterArray: 23 | parameter = parameter.split() 24 | for subparameter in parameter: 25 | postQueryParameters.append(subparameter) 26 | 27 | resultDict = postToUpstash(postQueryParameters) 28 | 29 | return resultDict 30 | 31 | 32 | # Returns event details from the event given. 33 | def getEvent(eventName): 34 | postQueryParameters = ['GET', eventName] 35 | date = postToUpstash(postQueryParameters) 36 | 37 | timeDiff = diffWithTodayFromString(date) 38 | totalTime = totalTimefromString(date) 39 | mergedDict = [date, timeDiff, totalTime] 40 | return mergedDict 41 | 42 | # Fetches all keys (events) from the database 43 | def getAllKeys(): 44 | return postToUpstash(['KEYS', '*']) 45 | 46 | # Deletes given event from the database. 47 | def removeEvent(eventName): 48 | postQueryParameters = ['DEL', eventName] 49 | resultDict = postToUpstash(postQueryParameters) 50 | return resultDict 51 | 52 | 53 | # Handles set request by parsing and configuring setEvent function parameters. 54 | def setHandler(commandArray): 55 | eventType = commandArray.pop(0) 56 | date = commandArray.pop(0) 57 | user = commandArray.pop(0) 58 | 59 | if eventType == "birthday": 60 | listName = "birthday-" + user 61 | return setEvent( [listName, date] ) 62 | 63 | elif eventType == "anniversary": 64 | listName = "anniversary-" + user 65 | return setEvent( [listName, date] ) 66 | 67 | elif eventType == "custom": 68 | message = "" 69 | for string in commandArray: 70 | message += string + "_" 71 | 72 | listName = "custom-" + user + "-" + message 73 | user = commandArray[1] 74 | return setEvent( [listName, date] ) 75 | else: 76 | return 77 | 78 | # Handles get-all requests. 79 | def getAllHandler(commandArray): 80 | filterParameter = None 81 | if len(commandArray) == 1: 82 | filterParameter = commandArray[0] 83 | 84 | allKeys = getAllKeys() 85 | birthdays = [] 86 | anniversaries = [] 87 | customs = [] 88 | 89 | slackUsers = allSlackUsers() 90 | 91 | stringResult = "\n" 92 | for key in allKeys: 93 | if key[0] == 'b': 94 | birthdays.append(key) 95 | elif key[0] == 'a': 96 | anniversaries.append(key) 97 | elif key[0] == 'c': 98 | customs.append(key) 99 | 100 | if filterParameter is None or filterParameter == "birthday": 101 | stringResult += "Birthdays:\n" 102 | for bday in birthdays: 103 | tag = bday.split('-')[1] 104 | username = tag[1:] 105 | realName = getRealName(slackUsers, username) 106 | details = getEvent(bday) 107 | 108 | stringResult += "`{}` ({}): {} - `{} days` remaining!\n".format(tag, realName, details[0], details[1]) 109 | 110 | if filterParameter is None or filterParameter == "anniversary": 111 | stringResult += "\nAnniversaries:\n" 112 | for ann in anniversaries: 113 | tag = ann.split('-')[1] 114 | username = tag[1:] 115 | realName = getRealName(slackUsers, username) 116 | details = getEvent(ann) 117 | 118 | stringResult += "`{}` ({}): {} - `{} days` remaining!\n".format(tag, realName, details[0], details[1]) 119 | 120 | if filterParameter is None or filterParameter == "custom": 121 | stringResult += "\nCustom Reminders:\n" 122 | for cstm in customs: 123 | splitted = cstm.split('-') 124 | username = splitted[2] 125 | realName = getRealName(slackUsers, username) 126 | details = getEvent(cstm) 127 | 128 | stringResult += "`{}-{}` ({}): {}\n".format(splitted[1], splitted[2], getRealName(slackUsers, username), details[0]) 129 | 130 | return stringResult -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Birthday - Anniversary - Custom Reminder Slackbot 2 | This slackbot is written in python, using `AWS Chalice` to configure AWS Lambda and API Gateway automatically. Project uses Upstash Redis for database. 3 | 4 | ## Docs 5 | - ### [What the Bot Does](#what-this-bot-does) 6 | - ### [Configuring Upstash](#configure-upstash) 7 | - ### [Configuring Slack - 1](#configure-slack-bot-1) 8 | - ### [Configuring AWS Credentials](#configure-aws-credentials) 9 | - ### [Deployment using Chalice](#deploy-on-chalice) 10 | - #### [Running Locally](#run-locally) 11 | - #### [Deploying to AWS](#deploy-to-aws) 12 | - ### [Configuring Slack - 2](#configure-slack-bot-2) 13 | 14 | 15 | ### What The Bot Does 16 | 17 | 19 | 20 | * Commands: 21 | * set: 22 | * `/event set birthday ` : 23 | * Sets the birthday of the user. 24 | * `/event set anniversary ` : 25 | * Sets the anniversary for the user, when they started working there. 26 | * `/event set custom ` : 27 | * Sets a custom reminder using the message provided. 28 | * get-all: 29 | * `/event get-all birthday` : 30 | * Shows all birthdays that are set. 31 | * `/event get-all anniversary` : 32 | * Shows all anniversaries that are set. 33 | * `/event get-all custom` : 34 | * Shows all custom events that are set. 35 | * get: 36 | * `/event get birthday ` : 37 | * Shows the birthday details of the user. 38 | * `/event get anniversary ` : 39 | * Shows the anniversary details for the user, when they started working there. 40 | * `/event get custom (can be found with get-all)` : 41 | * Shows the custom event details using the message provided. 42 | * remove: 43 | * `/event remove birthday ` : 44 | * Removes the birthday of the user. 45 | * `/event remove anniversary ` : 46 | * Removes the anniversary for the user, when they started working there. 47 | * `/event remove custom (can be found with get-all)` : 48 | * Removes the custom event using the message provided. 49 | 50 | * If a single user is mentioned while setting the event: 51 | * When the actual event date comes, mentions them in the general channel with a celebratory message. 52 | * Sends a private message to everyone except the mentioned user before the actual date comes. 53 | #### Add some SS here. 54 | 55 | ### Configuring Upstash 56 | 57 | 1. Go to the [Upstash Console](https://console.upstash.com/) and create a new database 58 | 59 | #### Upstash environment 60 | Find the variables in the database details page in Upstash Console: 61 | `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` 62 | 63 | (These will be the env variables for chalice deployment) 64 | 65 | 66 | 67 | 68 | ### Configuring Slack - 1 69 | 70 | May need to revise permissions and what not 71 | 72 | 1. Go to [Slack API Apps Page](https://api.slack.com/apps): 73 | * Create new App 74 | * From Scratch 75 | * Name your app & pick a workspace 76 | * Go to Oauth & Permissions 77 | * Add the following scopes 78 | * channels:read 79 | * chat:write 80 | * chat:write.public 81 | * commands 82 | * groups:read 83 | * users:read 84 | * Install App to workspace 85 | * Basic Information --> Install Your App --> Install To Workspace 86 | 2. Note the variables (These will be the env variables for vercel deployment) : 87 | * `SLACK_SIGNING_SECRET`: 88 | * Go to Basic Information 89 | * App Credentials --> Signing Secret 90 | * `SLACK_BOT_TOKEN`: 91 | * Go to OAuth & Permissions 92 | * Bot User OAuth Token 93 | 94 | 95 | 96 | 97 | ### Configuring AWS Credentials 98 | 99 | 100 | (Taken from [Official Chalice Repo](https://github.com/aws/chalice). You can refer for more info there.) 101 | ``` 102 | $ mkdir ~/.aws 103 | $ cat >> ~/.aws/config 104 | [default] 105 | aws_access_key_id=YOUR_ACCESS_KEY_HERE 106 | aws_secret_access_key=YOUR_SECRET_ACCESS_KEY 107 | region=YOUR_REGION (such as us-west-2, us-west-1, etc) 108 | ``` 109 | 110 | ### Deployment Using Chalice 111 | 112 | * Install `chalice` if not installed: 113 | 114 | ` 115 | pip install chalice 116 | ` 117 | 118 | * Create chalice project by running: 119 | 120 | ` 121 | chalice new-project 122 | ` 123 | 124 | * Configure `config.json` file inside the `.chalice` directory by adding: 125 | ``` 126 | "environment_variables": { 127 | "UPSTASH_REST_URL": , 128 | "UPSTASH_TOKEN": , 129 | "SLACK_BOT_TOKEN": , 130 | "SLACK_SIGNING_SECRET": , 131 | "NOTIFY_TIME_LIMIT": "" 132 | } 133 | ``` 134 | ### Running Locally 135 | 136 | 137 | To run locally: 138 | 139 | chalice local 140 | 141 | Using services such as ngrok, tunnel the relevant port, then use the public domain for Slack. 142 | ### Deploy to AWS Lambda and Gateway 143 | 144 | 145 | chalice deploy 146 | 147 | Use the `REST API URL` for Slack. 148 | ### Configuring Slack - 2 149 | 150 | 151 | * After deployment, you can use the provided `REST API URL`. 152 | 153 | 1. Go to [Slack API Apps Page](https://api.slack.com/apps) and choose relevant app: 154 | * Go to Slash Commands: 155 | * Create New Command: 156 | * Command : `event` 157 | * Request URL : `` 158 | * Configure the rest however you like. 159 | 160 | After these changes, Slack may require reinstalling of the app. 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /birthday-slackbot/app.py: -------------------------------------------------------------------------------- 1 | from chalice import Chalice, Cron, Rate 2 | import os 3 | import random 4 | from datetime import date 5 | from chalicelib.utils import responseToDict, postToChannel, diffWithTodayFromString, allSlackUsers, sendDm, validateRequest, convertToCorrectMention 6 | from chalicelib.upstash import setHandler, getAllHandler, getEvent, getAllKeys, removeEvent 7 | 8 | app = Chalice(app_name='birthday-slackbot') 9 | NOTIFY_TIME_LIMIT = int(os.getenv("NOTIFY_TIME_LIMIT")) 10 | 11 | 12 | # Sample route for get requests. 13 | @app.route('/', methods=["GET"]) 14 | def something(): 15 | return { 16 | "Hello": "World" 17 | } 18 | 19 | # Configuring POST request endpoint. 20 | # Command is parsed and handled/directed to handler 21 | @app.route('/', methods=["POST"], content_types=["application/x-www-form-urlencoded"]) 22 | def index(): 23 | 24 | # Parse the body for ease of use 25 | r = responseToDict(app.current_request.raw_body) 26 | headers = app.current_request.headers 27 | 28 | # Check validity of the request. 29 | if not validateRequest(headers, r): 30 | return {"Status": "Validation failed."} 31 | 32 | 33 | commandArray = r['text'].split() 34 | command = commandArray.pop(0) 35 | 36 | try: 37 | if command == "set": 38 | setHandler(commandArray) 39 | return { 40 | 'response_type': "ephemeral", 41 | 'text': "Set the event." 42 | } 43 | 44 | elif command == "get": 45 | eventType = commandArray[0] 46 | eventName = eventType + "-" + commandArray[1] 47 | resultDict = getEvent(eventName) 48 | return { 49 | 'response_type': "ephemeral", 50 | 'text': "`{}` Details:\n\n Date: {}\nRemaining: {} days!".format(eventName, resultDict[0], resultDict[1]) 51 | } 52 | 53 | elif command == "get-all": 54 | 55 | stringResult = getAllHandler(commandArray) 56 | return { 57 | 'response_type': "ephemeral", 58 | 'text': "{}".format(stringResult) 59 | } 60 | 61 | elif command == "remove": 62 | eventName = "{}-{}".format(commandArray[0], commandArray[1]) 63 | removeEvent(eventName) 64 | return { 65 | 'response_type': "ephemeral", 66 | 'text': "Removed the event." 67 | } 68 | else: 69 | return { 70 | 'response_type': "ephemeral", 71 | 'text': "Wrong usage of the command." 72 | } 73 | except: 74 | print("some stuff") 75 | return { 76 | 'response_type': "ephemeral", 77 | 'text': "Some problem occured. Please check your command." 78 | } 79 | 80 | 81 | # Run at 10:00 am (UTC) every day. 82 | @app.schedule(Cron(0, 10, '*', '*', '?', '*')) 83 | def periodicCheck(event): 84 | allKeys = getAllKeys() 85 | for key in allKeys: 86 | handleEvent(key) 87 | 88 | 89 | # Generic event is parsed and directed to relevant handlers. 90 | def handleEvent(eventName): 91 | eventSplitted = eventName.split('-') 92 | 93 | eventType = eventSplitted[0] 94 | 95 | # discard @ or ! as a first character 96 | personName = eventSplitted[1][1:] 97 | personMention = convertToCorrectMention(personName) 98 | 99 | eventDict = getEvent(eventName) 100 | remainingDays = eventDict[1] 101 | totalTime = eventDict[2] 102 | 103 | 104 | if eventType == "birthday": 105 | birthdayHandler(personMention, personName, remainingDays) 106 | 107 | elif eventType == "anniversary": 108 | anniversaryHandler(personMention, personName, remainingDays, totalTime) 109 | 110 | elif eventType == "custom": 111 | eventMessage = "Not specified" 112 | if len(eventSplitted) == 3: 113 | eventMessage = eventSplitted[2] 114 | customHandler(eventMessage, personMention, personName, remainingDays) 115 | 116 | # Handles birthday events. 117 | def birthdayHandler(personMention, personName, remainingDays): 118 | if remainingDays == 0: 119 | sendRandomBirthdayToChannel('general', personMention) 120 | if remainingDays <= NOTIFY_TIME_LIMIT: 121 | dmEveryoneExcept("{} day(s) until {}'s birthday!".format(remainingDays, personMention), personName) 122 | 123 | # Handles anniversary events. 124 | def anniversaryHandler(personMention, personName, remainingDays, totalTime): 125 | if remainingDays == 0: 126 | sendRandomAnniversaryToChannel('general', personMention, totalTime) 127 | if remainingDays <= NOTIFY_TIME_LIMIT: 128 | dmEveryoneExcept("{} day(s) until {}'s anniversary! It will be {} year(s) since they joined!".format(remainingDays, personMention, totalTime), personName) 129 | 130 | # Handles custom events. 131 | def customHandler(eventMessage, personMention, personName, remainingDays): 132 | if remainingDays == 0: 133 | postToChannel('general', "`{}` is here {}!".format(eventMessage, personMention)) 134 | elif remainingDays <= NOTIFY_TIME_LIMIT: 135 | dmEveryoneExcept("{} day(s) until {} `{}`!".format(remainingDays, personMention, eventMessage), personName) 136 | 137 | 138 | # Sends private message to everyone except for the person given. 139 | def dmEveryoneExcept(message, person): 140 | usersAndIds = allSlackUsers() 141 | for user in usersAndIds: 142 | if user[0] != person: 143 | sendDm(user[1], message) 144 | 145 | 146 | # Sends randomly chosen birthday message to specified channel. 147 | def sendRandomBirthdayToChannel(channel, personMention): 148 | messageList = [ 149 | "Happy Birthday {}! Wishing you the best!".format(personMention), 150 | "Happy Birthday {}! Wishing you a happy age!".format(personMention), 151 | "Happy Birthday {}! Wishing you a healthy, happy life!".format(personMention), 152 | ] 153 | message = random.choice(messageList) 154 | return postToChannel('general', message) 155 | 156 | # Sends randomly chosen anniversary message to specified channel. 157 | def sendRandomAnniversaryToChannel(channel, personMention, totalTime): 158 | messageList = [ 159 | "Today is the anniversary of {} joining! It has been {} years since they joined!".format(personMention, totalTime - 1), 160 | "Celebrating the anniversary of {} joining! It has been {} years!".format(personMention, totalTime - 1), 161 | "Congratulating {} for entering {}(th) year here!".format(personMention, totalTime), 162 | ] 163 | message = random.choice(messageList) 164 | return postToChannel('general', message) 165 | 166 | 167 | # We want to run our event handlers when the project is deployed/redeployed. 168 | allKeys = getAllKeys() 169 | for key in allKeys: 170 | handleEvent(key) --------------------------------------------------------------------------------