text: #!/usr/bin/env python3
  # -*- coding: utf-8 -*-
  import os
  from datetime import date, timedelta
  import calendar
  from slack_sdk import WebClient
  import argparse
  import subprocess
  import re
  from collections import defaultdict
  import gzip
  
  # Example:
  # python loginbonus.py
  #
  
  post_to_slack = True
  slacktoken_file = 'slack_token'
  
  excluded_members = set()
  
  channel_name = '自動アナウンス'
  appdir = 'var/loginbonus/'
  history_file_format = '{}.txt' # date
  ts_file = 'ts-loginbonus'
  excluded_members_file = 'excluded_members.txt'
  post_format = {
      'post_header_format' : '＊【{}のログインボーナス】＊',
      'post_line_format' : '<@{}> さん', # member
      'post_nobody' : 'ログインした人はいません。',
      'post_footer' : '\n以上の方にログインボーナスが付与されます。\nおめでとうございます！ :sparkles:',
  }
  N_ranking = 5
  post_format_ranking = {
      'post_header_format' : '＊【{}のログインボーナス ベスト{}】＊',
      'post_line_format' : '<@{}> さん', # member
      'post_line_format' : '{}位：{}<@{}> さん ({}ポイント)', # rank, mark, slackid, point
      'post_remain_format' : 'その他2ポイント以上：{}',
      'rank_marks' : [':first_place_medal:',':second_place_medal:',':third_place_medal:'],
      'other_mark' : ':sparkles:',
      'post_nobody' : 'ログインした人はいませんでした :scream:',
      'post_footer' : '\nおめでとうございます！ :tada:',
  }
  post_format_list = {
      'post_header_format' : '＊【現在の利用者一覧】＊',
      'post_footer' : '以上、{}名です。 :sparkles:',
  }
  
  loginname_format = re.compile(r'u[0-9]{6}[a-z]')
  
  def get_channel_list(client, limit=200):
      params = {
          'exclude_archived': 'true',
          'types': 'public_channel',
          'limit': str(limit),
          }
      channels = client.api_call('conversations.list', params=params)
      if channels['ok']:
          return channels['channels']
      else:
          return None
  
  def get_channel_id(client, channel_name):
      channels = filter(lambda x: x['name']==channel_name , get_channel_list(client))
      target = None
      for c in channels:
          if target is not None:
              break
          else:
              target = c
      if target is None:
          return None
      else:
          return target['id']
  
  def auth_logins(day):
      authlogs = (
          ['/var/log/auth.log']
          + ['/var/log/auth.log.{}'.format(i) for i in range(1,10)]
          + ['/var/log/auth.log.{}.gz'.format(i) for i in range(1,10)]
      )
      auth_lines = []
      for authlog in authlogs:
          if os.path.isfile(authlog):
              if authlog[-3:] == '.gz':
                  f = gzip.open(authlog, 'rt')
              else:
                  f = open(authlog)
              auth_lines.extend(f.readlines())            
              f.close()
      logins = set()
      month_str = day.strftime('%b')
      day_str = str(int(day.strftime('%d')))
      regex = re.compile(r'\s*Accepted publickey for (\S+) from')
      for line in auth_lines:
          m, d = line.split()[:2]
          if m == month_str and d == day_str:
              hit = regex.match(line.split(':')[3])
              if hit:
                  logins.add(hit.groups()[0])
      return logins
  
  def login_members(members, name, day, auth):
      daystr = day.strftime('%Y%m%d')
      since = daystr + '000000'
      till = daystr + '235959'
      last_out = subprocess.run(
                      ['last', '-s', since, '-t', till],
                      encoding='utf-8', stdout=subprocess.PIPE,
                      ).stdout.splitlines()[:-2]
      logins = { line.split()[0] for line in last_out }
      if auth:
          logins |= auth_logins(day)
      ret = set()
      for m in members:
          m_name = name[m].strip()
          if not m_name:
              continue
          m_loginname = m_name.split('_')[-1]
          if loginname_format.fullmatch(m_loginname) and m_loginname in logins:
              groups = subprocess.run(
                      ['groups', m_loginname],
                      encoding='utf-8', stdout=subprocess.PIPE,
                      ).stdout.split()[2:]
              if 'users' in groups:
                  ret.add(m)
  
      return ret
  
  def login_days(members, name, endofmonth, auth):
      year = endofmonth.year
      month = endofmonth.month
      days = endofmonth.day
      scores = defaultdict(int)
      for day in range(1,days+1):
          for m in login_members(members, name, date(year,month,day), auth):
              scores[m] += 1
  
      return sorted(scores.items(), key=lambda x: -x[1])
  
  
  if __name__ == '__main__':
      parser = argparse.ArgumentParser()
      parser.add_argument('--noslack', help='do not post to slack.',
                          action='store_true')
      parser.add_argument('--mute', help='post in thread without showing on channel.',
                          action='store_true')
      parser.add_argument('--solopost',
                          help='post an idependent message out of the thread, not destroying previous thread info',
                          action='store_true')
      parser.add_argument('--list', help='list all the members.',
                          action='store_true')
      parser.add_argument('-c', '--channel', default=channel_name,
                          help='slack channel to read & post.')
      parser.add_argument('-o', '--outchannel', default=None,
                          help='slack channel to post.')
      parser.add_argument('--slacktoken', default=None,
                          help='slack bot token.')
      parser.add_argument('--oncemore', action='store_true',
                          help='execute even if it has been already done for the day.')
      parser.add_argument('--day', default=None,
                          help='specify a day in %%Y%%m%%d format.')
      parser.add_argument('--ranking', action='store_true',
                          help='show monthly ranking.')
      parser.add_argument('--auth', action='store_true',
                          help='Use auth.log, in addition to wtmp.log.')
      parser.add_argument('--appdir', default=appdir,
                          help='Set application directory, as a relative path from $HOME. (default: {})'.format(appdir))
      args = parser.parse_args()
  
      if args.noslack:
          post_to_slack = False
      channel_name = args.channel
  
      base_dir = os.path.join(os.environ['HOME'], args.appdir)
      history_dir = os.path.join(base_dir, 'history/')
  
      slacktoken_file_path = os.path.join(base_dir, slacktoken_file)
      history_file_path_format = os.path.join(history_dir, history_file_format)
      excluded_members_file_path = os.path.join(base_dir, excluded_members_file)
  
      if args.day and isinstance(args.day,str) and len(args.day)>=8:
          year = int(args.day[:4])
          month = int(args.day[4:6])
          day = int(args.day[6:8])
          today = date(year, month, day)
      else:
          today = date.today() - timedelta(days=1) # actually yesterday
      ADfirst = date(1,1,1) # AD1.1.1 is Monday
      today_id = (today-ADfirst).days
      history_file_path = history_file_path_format.format(today_id)
  
      if (not args.list and not args.ranking) and (not args.oncemore) and os.path.exists(history_file_path):
          exit()
      if args.list:
          post_format.update(post_format_list)
      elif args.ranking:
          post_format.update(post_format_ranking)
      for k, v in post_format.items():
          globals()[k] = v
  
      if args.slacktoken:
          token = args.slacktoken
      else:
          with open(slacktoken_file_path, 'r') as f:
              token = f.readline().rstrip()
      web_client = WebClient(token=token)
      channel_id = get_channel_id(web_client, channel_name)
      my_id = web_client.api_call('auth.test')['user_id']
  
      logins = set()
      if os.path.exists(excluded_members_file_path):
          with open(excluded_members_file_path, 'r') as f:
              lines = f.readlines()
              for line in lines:
                  excluded_members.add(line.rstrip().split()[1])
      members_info = web_client.api_call('users.list')['members']
      name = dict()
      for member in members_info:
          display_name = member['profile']['display_name']
          real_name = member['profile']['real_name']
          if display_name:
              name[member['id']] = display_name
          else:
              name[member['id']] = real_name
      members = set([ member['id'] for member in members_info if not member['deleted']]) - excluded_members
      members.discard(my_id)
      N_members = len(members)
      if args.list:
          header_data = (None,)
          logins = set(members)
      elif args.ranking:
          thismonth_days = calendar.monthrange(today.year, today.month)[1]
          thismonthend = date(today.year, today.month, thismonth_days)
          prev_n, prev_s = -1, 50
          ranking = login_days(members, name, thismonthend, args.auth)
          logins = []
          remain_str_list = []
          for n, r in enumerate(ranking):
              m, s = r
              if s == prev_s:
                  if prev_n < len(rank_marks):
                      mark = rank_marks[prev_n]
                  else:
                      mark = other_mark
                  logins.append((prev_n+1, mark, m, s))
              elif n < N_ranking:
                  if n < len(rank_marks):
                      mark = rank_marks[n]
                  else:
                      mark = other_mark
                  prev_n, prev_s = n, s
                  logins.append((n+1, mark, m, s))
              elif s >= 2:
                  remain_str_list.append('<@{}>'.format(m))
              else:
                  break
          if remain_str_list:
              post_footer =  post_remain_format.format('、'.join(remain_str_list)) + '\n' + post_footer
          header_data = ('{}月'.format(today.month), N_ranking)
      else:
          logins = login_members(members, name, today, args.auth)
          # write the new history
          with open(history_file_path, 'w') as f:
              for m in logins:
                  print(m, file=f)
          header_data = ('{}月{}日'.format(today.month, today.day),)
  
      post_lines = [post_header_format.format(*header_data)]
      if logins:
          for m in logins:
              if isinstance(m, tuple):
                  post_lines.append(post_line_format.format(*m))
              else:
                  post_lines.append(post_line_format.format(m))
          post_lines.append(
              post_footer.format(N_members)
          )
      else:
          post_lines.append(post_nobody)
      message = '\n'.join(post_lines)
  
      if not post_to_slack:
          print('App ID:', my_id)
          print(message)
      elif len(post_lines) > 2:
          if args.outchannel:
              channel_id = get_channel_id(web_client, args.outchannel)
          params={
              'channel': channel_id,
              'text': message,
          }
          os.chdir(history_dir)
          if os.path.isfile(ts_file):
              with open(ts_file, 'r') as f:
                  ts = f.readline().rstrip()
                  if not args.solopost:
                      params['thread_ts'] = ts
                      if not args.mute:
                          params['reply_broadcast'] = 'True'
          else:
              ts = None
          response = web_client.api_call(
              'chat.postMessage',
              params=params
          )
          posted_data = response.data
          if ts is None:
              ts = posted_data['ts']
              with open(ts_file, 'w') as f:
                  print(ts, file=f)
  