slack_autoarchive.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env python
  2. """
  3. This program lets you do archive slack channels which are no longer active.
  4. """
  5. # standard imports
  6. from datetime import datetime
  7. import os
  8. import sys
  9. import time
  10. import json
  11. # not standard imports
  12. import requests
  13. from config import get_channel_reaper_settings
  14. from utils import get_logger
  15. class ChannelReaper():
  16. """
  17. This class can be used to archive slack channels.
  18. """
  19. def __init__(self):
  20. self.settings = get_channel_reaper_settings()
  21. self.logger = get_logger('channel_reaper', './audit.log')
  22. def get_whitelist_keywords(self):
  23. """
  24. Get all whitelist keywords. If this word is used in the channel
  25. purpose or topic, this will make the channel exempt from archiving.
  26. """
  27. keywords = []
  28. if os.path.isfile('whitelist.txt'):
  29. with open('whitelist.txt') as filecontent:
  30. keywords = filecontent.readlines()
  31. # remove whitespace characters like `\n` at the end of each line
  32. keywords = map(lambda x: x.strip(), keywords)
  33. whitelist_keywords = self.settings.get('whitelist_keywords')
  34. if whitelist_keywords:
  35. keywords = keywords + whitelist_keywords.split(',')
  36. return list(keywords)
  37. def get_channel_alerts(self):
  38. """Get the alert message which is used to notify users in a channel of archival. """
  39. archive_msg = """
  40. This channel has had no activity for %d days. It is being auto-archived.
  41. If you feel this is a mistake you can <https://get.slack.help/hc/en-us/articles/201563847-Archive-a-channel#unarchive-a-channel|unarchive this channel>.
  42. This will bring it back at any point. In the future, you can add '%%noarchive' to your channel topic or purpose to avoid being archived.
  43. This script was run from this repo: https://github.com/Symantec/slack-autoarchive
  44. """ % self.settings.get('days_inactive')
  45. alerts = {'channel_template': archive_msg}
  46. if os.path.isfile('templates.json'):
  47. with open('templates.json') as filecontent:
  48. alerts = json.load(filecontent)
  49. return alerts
  50. # pylint: disable=too-many-arguments
  51. def slack_api_http(
  52. self,
  53. api_endpoint=None,
  54. payload=None,
  55. method='GET',
  56. # pylint: disable=unused-argument
  57. retry=True,
  58. retry_delay=0):
  59. """ Helper function to query the slack api and handle errors and rate limit. """
  60. # pylint: disable=no-member
  61. uri = 'https://slack.com/api/' + api_endpoint
  62. payload['token'] = self.settings.get('slack_token')
  63. try:
  64. # Force request to take at least 1 second. Slack docs state:
  65. # > In general we allow applications that integrate with Slack to send
  66. # > no more than one message per second. We allow bursts over that
  67. # > limit for short periods.
  68. if retry_delay > 0:
  69. time.sleep(retry_delay)
  70. if method == 'POST':
  71. response = requests.post(uri, data=payload)
  72. else:
  73. response = requests.get(uri, params=payload)
  74. if response.status_code == requests.codes.ok and 'error' in response.json(
  75. ) and response.json()['error'] == 'not_authed':
  76. self.logger.error(
  77. 'Need to setup auth. eg, SLACK_TOKEN=<secret token> python slack-autoarchive.py'
  78. )
  79. sys.exit(1)
  80. elif response.status_code == requests.codes.ok and response.json(
  81. )['ok']:
  82. return response.json()
  83. elif response.status_code == requests.codes.too_many_requests:
  84. retry_timeout = float(response.headers['Retry-After'])
  85. # pylint: disable=too-many-function-args
  86. return self.slack_api_http(api_endpoint, payload, method,
  87. False, retry_timeout)
  88. except Exception as error_msg:
  89. raise Exception(error_msg)
  90. return None
  91. def get_all_channels(self):
  92. """ Get a list of all non-archived channels from slack channels.list. """
  93. payload = {'exclude_archived': 1}
  94. api_endpoint = 'channels.list'
  95. channels = self.slack_api_http(api_endpoint=api_endpoint,
  96. payload=payload)['channels']
  97. all_channels = []
  98. for channel in channels:
  99. all_channels.append({
  100. 'id': channel['id'],
  101. 'name': channel['name'],
  102. 'created': channel['created'],
  103. 'num_members': channel['num_members']
  104. })
  105. return all_channels
  106. def get_last_message_timestamp(self, channel_history, too_old_datetime):
  107. """ Get the last message from a slack channel, and return the time. """
  108. last_message_datetime = too_old_datetime
  109. last_bot_message_datetime = too_old_datetime
  110. if 'messages' not in channel_history:
  111. return (last_message_datetime, False) # no messages
  112. for message in channel_history['messages']:
  113. if 'subtype' in message and message[
  114. 'subtype'] in self.settings.get('skip_subtypes'):
  115. continue
  116. last_message_datetime = datetime.fromtimestamp(float(
  117. message['ts']))
  118. break
  119. # for folks with the free plan, sometimes there is no last message,
  120. # then just set last_message_datetime to epoch
  121. if not last_message_datetime:
  122. last_bot_message_datetime = datetime.utcfromtimestamp(0)
  123. # return bot message time if there was no user message
  124. if too_old_datetime >= last_bot_message_datetime > too_old_datetime:
  125. return (last_bot_message_datetime, False)
  126. return (last_message_datetime, True)
  127. def is_channel_disused(self, channel, too_old_datetime):
  128. """ Return True or False depending on if a channel is "active" or not. """
  129. num_members = channel['num_members']
  130. payload = {'inclusive': 0, 'oldest': 0, 'count': 50}
  131. api_endpoint = 'channels.history'
  132. payload['channel'] = channel['id']
  133. channel_history = self.slack_api_http(api_endpoint=api_endpoint,
  134. payload=payload)
  135. (last_message_datetime, is_user) = self.get_last_message_timestamp(
  136. channel_history, datetime.fromtimestamp(float(channel['created'])))
  137. # mark inactive if last message is too old, but don't
  138. # if there have been bot messages and the channel has
  139. # at least the minimum number of members
  140. min_members = self.settings.get('min_members')
  141. has_min_users = (min_members == 0 or min_members > num_members)
  142. return last_message_datetime <= too_old_datetime and (not is_user
  143. or has_min_users)
  144. # If you add channels to the WHITELIST_KEYWORDS constant they will be exempt from archiving.
  145. def is_channel_whitelisted(self, channel, white_listed_channels):
  146. """ Return True or False depending on if a channel is exempt from being archived. """
  147. # self.settings.get('skip_channel_str')
  148. # if the channel purpose contains the string self.settings.get('skip_channel_str'), we'll skip it.
  149. info_payload = {'channel': channel['id']}
  150. channel_info = self.slack_api_http(api_endpoint='channels.info',
  151. payload=info_payload,
  152. method='GET')
  153. channel_purpose = channel_info['channel']['purpose']['value']
  154. channel_topic = channel_info['channel']['topic']['value']
  155. if self.settings.get(
  156. 'skip_channel_str') in channel_purpose or self.settings.get(
  157. 'skip_channel_str') in channel_topic:
  158. return True
  159. # check the white listed channels (file / env)
  160. for white_listed_channel in white_listed_channels:
  161. wl_channel_name = white_listed_channel.strip('#')
  162. if wl_channel_name in channel['name']:
  163. return True
  164. return False
  165. def send_channel_message(self, channel_id, message):
  166. """ Send a message to a channel or user. """
  167. payload = {
  168. 'channel': channel_id,
  169. 'username': 'channel_reaper',
  170. 'icon_emoji': ':ghost:',
  171. 'text': message
  172. }
  173. api_endpoint = 'chat.postMessage'
  174. self.slack_api_http(api_endpoint=api_endpoint,
  175. payload=payload,
  176. method='POST')
  177. def archive_channel(self, channel, alert):
  178. """ Archive a channel, and send alert to slack admins. """
  179. api_endpoint = 'channels.archive'
  180. stdout_message = 'Archiving channel... %s' % channel['name']
  181. self.logger.info(stdout_message)
  182. if not self.settings.get('dry_run'):
  183. channel_message = alert.format(self.settings.get('days_inactive'))
  184. self.send_channel_message(channel['id'], channel_message)
  185. payload = {'channel': channel['id']}
  186. self.slack_api_http(api_endpoint=api_endpoint, payload=payload)
  187. self.logger.info(stdout_message)
  188. def send_admin_report(self, channels):
  189. """ Optionally this will message admins with which channels were archived. """
  190. if self.settings.get('admin_channel'):
  191. channel_names = ', '.join('#' + channel['name']
  192. for channel in channels)
  193. admin_msg = 'Archiving %d channels: %s' % (len(channels),
  194. channel_names)
  195. if self.settings.get('dry_run'):
  196. admin_msg = '[DRY RUN] %s' % admin_msg
  197. self.send_channel_message(self.settings.get('admin_channel'),
  198. admin_msg)
  199. def main(self):
  200. """
  201. This is the main method that checks all inactive channels and archives them.
  202. """
  203. if self.settings.get('dry_run'):
  204. self.logger.info(
  205. 'THIS IS A DRY RUN. NO CHANNELS ARE ACTUALLY ARCHIVED.')
  206. whitelist_keywords = self.get_whitelist_keywords()
  207. alert_templates = self.get_channel_alerts()
  208. archived_channels = []
  209. for channel in self.get_all_channels():
  210. sys.stdout.write('.')
  211. sys.stdout.flush()
  212. channel_whitelisted = self.is_channel_whitelisted(
  213. channel, whitelist_keywords)
  214. channel_disused = self.is_channel_disused(
  215. channel, self.settings.get('too_old_datetime'))
  216. if (not channel_whitelisted and channel_disused):
  217. archived_channels.append(channel)
  218. self.archive_channel(channel,
  219. alert_templates['channel_template'])
  220. self.send_admin_report(archived_channels)
  221. if __name__ == '__main__':
  222. CHANNEL_REAPER = ChannelReaper()
  223. CHANNEL_REAPER.main()