Командная строка — это мощный интерфейс. Каждый, кто впервые погрузился в 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"]
Наслаждаться.

