Командная строка — это мощный интерфейс. Каждый, кто впервые погрузился в UNIX и выжил, чтобы рассказать об этом, может это подтвердить. Однако вам не нужно быть компьютерным гуру, чтобы взаимодействовать с командной строкой. В этой записи блога я хочу открыть для вас окно возможности раскрыть свой потенциал в чатах (Skype). Обратите внимание, что этот пост не ограничивается рамками Skype, но принципы, представленные здесь, могут быть применены к любому чату, который использует ваша команда.
1. Зачем создавать чат-робота?
Как правило, неопытные люди взаимодействуют с несколькими любителями командной строки в своей повседневной работе.
- Адресная строка браузера ( Awesome Bar в Firefox добился потрясающего прогресса в переносе моей когнитивной нагрузки в адресную строку браузера)
- Окно поиска в поисковой системе (автозаполнение, но DuckDuckGo предлагает здесь еще больше удивительных трюков )
- Поле ввода Microsoft Excel или подобное
- Вход в чат и обмен мгновенными сообщениями
- и т.п.
Командная строка является мощной, как указано выше. Он быстро печатается, но он также более подвержен ошибкам и более труден для обнаружения, в отличие, например, от интерфейса с меню или веб-приложения. Clickety-click способствует обнаружению, показывая вам варианты по мере вашего продвижения. Командная строка больше похожа на то, что вам нужно знать, что вы вводите, и вы делаете это за один проход — если есть ошибки, вы перезапускаете процесс с самого начала. По этой причине командная строка более эффективна для повторения задач: набор текста не имеет ограничения по скорости и исходит из вашей мышечной памяти. После того, как вы пройдете кривую изучения командной строки, все будет сложнее, лучше, быстрее, сильнее .
2. Skype-чат, универсальная командная строка
Вот пример из реальной жизни: менеджер в небольшой организации хотел лучше представить, что делают все члены их команды. Из-за характера работы, люди не присутствуют в офисе: они работают здесь и там в помещениях клиента.
Мы могли бы использовать хорошее веб-приложение или даже мобильное приложение, чтобы отслеживать задачи, над которыми все работают. Однако именно этот менеджер подумал: «Это слишком тяжелый вес». Экономическое обоснование состояло в том, что, когда ему звонит клиент, спрашивающий, выполнена ли эта задача или еще не запущена, он должен знать об этом в данный момент. Организация небольшая, каждый человек — гик и занимается «Принеси свои собственные устройства», поэтому приложение должно быть доступно всем, что у тебя под рукой.
Менеджер видел некоторые более ранние попытки автоматизации, которые мы предприняли с ботом чата Skype, Sevabot. Поэтому он спросил меня, можем ли мы создать приложение со списком задач, которое будет упрощено в чате через Skype: просто отметьте все, над чем вы работаете, и отметьте снова, когда вы закончите, и все это с помощью команд чата Skype.
Преимущества этого подхода
- Все в команде уже использовали Skype: люди знакомы с ним
- Skype известен своей работой на любом устройстве, в любой сети, везде
- Синхронизация Skype: даже в автономном режиме у вас есть некоторые статусные сообщения на вашем мобильном устройстве
- Командная строка эффективна: запись сообщения чата для вывода текущих задач может быть выполнена, даже если вы разговариваете по мобильному телефону. Это буквально занимает секунду.
- Написание простого приложения на основе команд имеет очень хорошее соотношение затрат и выгод: написание текстовых приложений — это быстрая задача, которую можно выполнить за пару часов (в отличие от создания веб-приложения, некоторых форм и т. Д.)
И вот как это работает:
3. Создание приложения чата с сохранением состояния с помощью Sevabot
Sevabot — это постоянный проект бота Skype с открытым исходным кодом . Раньше (версия 1.2) уже поддерживала запуск сценариев UNIX с помощью команд чата и HTTP-подключений для отправки уведомлений чата из внешних служб . Задача, стоящая перед нами, могла быть создана с небольшим количеством магии сценариев UNIX. Однако я связывался с Наото Йокоямой, который нуждался в более сложных командах чата в Skype, кроме простых триггеров. Основываясь на обсуждении и оригинальной работе Наото, мы создали поддержку сценариев в чате для Sevabot .
- Вы определяете свое (сохраняющее состояние) приложение чата как простой модуль и класс Python
- Модули перезаряжаемые
- Установка нового сценария с состоянием проста, как перетащить файл .py в правильную папку
- Сценарий может устанавливать обработчики событий для всех необработанных событий Skype4Py , включая обработку вызовов Skype
- Вы можете использовать фоновые процессы, таймеры, потоки, собственные расширения и всю мощь обычных долгосрочных приложений Python UNIX
Ниже приведен исходный код Python для приложения для управления задачами, описанного выше.
Некоторые заметки о модуле
- Мы сохраняем состояние скрипта на диске. Sevabot работает в облаке и может быть перезапущен в любой момент. Мы просто используем Python pickles , здесь базы данных не нужны.
- Каждый групповой чат имеет свой собственный список задач, поэтому можно мультиплексировать один экземпляр бота в разных командах и рабочих группах.
- Существует таймер, который будет толкать людей, если они забыли закрыть задачи, которые они начали.
4. Будущее
Далее я хотел бы попробовать следующее
- Автоматизация развертывания с помощью команд Skype ( поток Github )
- Проверяя, есть ли у продавцов, использующих Skype, повторяющиеся задачи, мы могли бы подтолкнуть их к боту.
- Размещение экземпляров Sevabot в облаке. Изображение Amazon EC2 составляет минимум 60 долларов в месяц. Слишком много для экземпляра бота. Я с нетерпением жду возможности увидеть, что Docker предоставит здесь в будущем, связанное с хостингом процессов в barebone UNIX.
- Как автоматизировать первоначальную настройку пользовательского интерфейса Skype?
5. Исходный код
Исходный код модуля также находится на Github и распространяется вместе с Sevabot 1.2 .
#!/sevabot """ Simple group chat task manager. This also serves as an example how to write stateful handlers. """ from __future__ import unicode_literals from threading import Timer from datetime import datetime import os import logging import pickle from collections import OrderedDict from sevabot.bot.stateful import StatefulSkypeHandler from sevabot.utils import ensure_unicode, get_chat_id logger = logging.getLogger("Tasks") # Set to debug only during dev logger.setLevel(logging.INFO) logger.debug("Tasks module level load import") # How long one can work on a task before we give a warning MAX_TASK_DURATION = 24*60*60 HELP_TEXT = """!tasks is a noteboard where virtual team members can share info which tasks they are currently working on. Commands ------------------------------ !tasks: This help text Start task: You start working on a task. When you started is recorded. Example: start task I am now working on new Sevabot module interface Stop task: Stop working on the current task. Example: stop task List tasks: List all tasks an people working on them. Example: list tasks Task lists are chat specific and the list is secure to the members of the chat. All commands are case-insensitive. """ class TasksHandler(StatefulSkypeHandler): """ Skype message handler class for the task manager. """ def __init__(self): """Use `init` method to initialize a handler. """ logger.debug("Tasks constructed") def init(self, sevabot): """ Set-up our state. This is called :param skype: Handle to Skype4Py instance """ logger.debug("Tasks init") self.sevabot = sevabot self.status_file = os.path.join(os.path.dirname(__file__), "sevabot-tasks.tmp") self.status = Status.read(self.status_file) self.commands = { "!tasks": self.help, "start task": self.start_task, "list tasks": self.list_tasks, "stop task": self.stop_task, } self.reset_timeout_notifier() def handle_message(self, msg, status): """Override this method to customize a handler. """ # Skype API may give different encodings # on different platforms body = ensure_unicode(msg.Body) logger.debug("Tasks handler got: %s" % body) # Parse the chat message to commanding part and arguments words = body.split(" ") lower = body.lower() if len(words) == 0: return False # Parse argument for two part command names if len(words) >= 2: desc = " ".join(words[2:]) else: desc = None chat_id = get_chat_id(msg.Chat) # Check if we match any of our commands for name, cmd in self.commands.items(): if lower.startswith(name): cmd(msg, status, desc, chat_id) return True return False def shutdown(self): """ Called when the module is reloaded. """ logger.debug("Tasks shutdown") self.stop_timeout_notifier() def save(self): """ Persistent our state. """ Status.write(self.status_file, self.status) def reset_timeout_notifier(self): """ Check every minute if there are overdue jobs """ self.notifier = Timer(60.0, self.check_overdue_jobs) self.notifier.daemon = True # Make sure CTRL+C works and does not leave timer blocking it self.notifier.start() def stop_timeout_notifier(self): """ """ self.notifier.cancel() def help(self, msg, status, desc, chat_id): """ Print help text to chat. """ # Make sure we don't trigger ourselves with the help text if not desc: msg.Chat.SendMessage(HELP_TEXT) def warn_overdue(self, chat_id, job): """ Generate overdue warning. """ self.sevabot.sendMessage(chat_id, "Task hanging: %s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started))) job.warned = True def check_overdue_jobs(self): """ Timer callback to go through jobs which might be not going forward. """ found = False logger.debug("Running overdue check") now = datetime.now() for chat_id, chat in self.status.chats.items(): for job in chat.values(): if (now - job.started).total_seconds() > MAX_TASK_DURATION and not job.warned: found = True self.warn_overdue(chat_id, job) if found: logger.debug("Found overdue jobs") self.save() else: logger.debug("Did not found overdue jobs") # http://www.youtube.com/watch?v=ZEQydmaPjF0 self.reset_timeout_notifier() def start_task(self, msg, status, desc, chat_id): """ Command handler. """ if desc.strip() == "": msg.Chat.SendMessage("Please give task description also") return tasks = self.status.get_tasks(chat_id) existing_job = tasks.get(msg.Sender.Handle, None) if existing_job: msg.Chat.SendMessage("Stopped existing task %s" % existing_job.desc) job = Job(msg.Sender.FullName, datetime.now(), desc) tasks = self.status.get_tasks(chat_id) tasks[msg.Sender.Handle] = job self.save() msg.Chat.SendMessage("%s started working on %s." % (job.real_name, job.desc)) def list_tasks(self, msg, status, desc, chat_id): """ Command handler. """ jobs = self.status.get_tasks(chat_id).values() if len(jobs) == 0: msg.Chat.SendMessage("No active tasks for anybody") for job in jobs: msg.Chat.SendMessage("%s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started))) def stop_task(self, msg, status, desc, chat_id): """ Command handler. """ tasks = self.status.get_tasks(chat_id) if msg.Sender.Handle in tasks: job = tasks[msg.Sender.Handle] del tasks[msg.Sender.Handle] msg.Chat.SendMessage("%s finished" % job.desc) else: msg.Chat.SendMessage("%s had no active task" % msg.Sender.FullName) self.save() class Status: """ Stored pickled state of the tasks. Use Python pickling serialization for making status info persistent. """ def __init__(self): # Chat id -> OrderedDict() of jobs mappings self.chats = dict() @classmethod def read(cls, path): """ Read status file. Return fresh status if file does not exist. """ if not os.path.exists(path): # Status file do not exist, get default status return Status() f = open(path, "rb") try: return pickle.load(f) finally: f.close() @classmethod def write(cls, path, status): """ Write status file """ f = open(path, "wb") pickle.dump(status, f) f.close() def get_tasks(self, chat_id): """ Get jobs of a particular chat. """ if not chat_id in self.chats: # Skype username -> Task instance mappings self.chats[chat_id] = OrderedDict() return self.chats[chat_id] class Job: """ Tracks who is doing what """ def __init__(self, real_name, started, desc): """ :param started: datetime when the job was started """ self.started = started self.desc = desc self.real_name = real_name # Have we given timeout warning for this job self.warned = False # The following has been # ripped off from https://github.com/imtapps/django-pretty-times/blob/master/pretty_times/pretty.py _ = lambda x: x def pretty_time_delta(time): now = datetime.now(time.tzinfo) if time > now: past = False diff = time - now else: past = True diff = now - time days = diff.days if days is 0: return get_small_increments(diff.seconds, past) else: return get_large_increments(days, past) def get_small_increments(seconds, past): if seconds < 10: result = _('just now') elif seconds < 60: result = _pretty_format(seconds, 1, _('seconds'), past) elif seconds < 120: result = past and _('a minute ago') or _('in a minute') elif seconds < 3600: result = _pretty_format(seconds, 60, _('minutes'), past) elif seconds < 7200: result = past and _('an hour ago') or _('in an hour') else: result = _pretty_format(seconds, 3600, _('hours'), past) return result def get_large_increments(days, past): if days == 1: result = past and _('yesterday') or _('tomorrow') elif days < 7: result = _pretty_format(days, 1, _('days'), past) elif days < 14: result = past and _('last week') or _('next week') elif days < 31: result = _pretty_format(days, 7, _('weeks'), past) elif days < 61: result = past and _('last month') or _('next month') elif days < 365: result = _pretty_format(days, 30, _('months'), past) elif days < 730: result = past and _('last year') or _('next year') else: result = _pretty_format(days, 365, _('years'), past) return result def _pretty_format(diff_amount, units, text, past): pretty_time = (diff_amount + units / 2) / units if past: base = "%(amount)d %(quantity)s ago" else: base = "%(amount)d %(quantity)s" return base % dict(amount=pretty_time, quantity=text) # Export the instance to Sevabot sevabot_handler = TasksHandler() __all__ = ["sevabot_handler"]
Наслаждаться.