├── linot
├── __init__.py
├── services
│ ├── twitch_notifier
│ │ ├── __init__.py
│ │ ├── twitch_engine.py
│ │ └── service.py
│ ├── __init__.py
│ └── service_base.py
├── config.py
├── command_submitter.py
├── base_interface.py
├── logger.py
├── interfaces
│ ├── __init__.py
│ ├── test_interface.py
│ └── line_interface.py
├── linot.py
├── command_server.py
└── arg_parser.py
├── tests
├── __init__.py
├── interfaces
│ ├── __init__.py
│ ├── test_interfaces.py
│ └── test_line_interface.py
├── services
│ ├── __init__.py
│ ├── test_service_base.py
│ └── test_twitch_notifier.py
├── test_base_interface.py
├── test_command_submitter.py
├── test_arg_parser.py
└── test_command_server.py
├── run.py
├── nose.cfg
├── generate_config.py
├── LICENSE
└── README.md
/linot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/interfaces/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/linot/services/twitch_notifier/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | from linot import linot
2 | if __name__ == '__main__':
3 | linot.main()
4 |
--------------------------------------------------------------------------------
/nose.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | verbosity=3
3 | with-coverage=1
4 | cover-erase=1
5 | cover-inclusive=1
6 | cover-package=linot
7 | cover-html=1
8 | cover-branches=1
9 | tests=tests/
10 |
--------------------------------------------------------------------------------
/linot/config.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import cPickle
3 |
4 |
5 | class _Config(object):
6 | def __init__(self, config_file):
7 | self._config_dist = cPickle.load(open(config_file, 'rb'))
8 |
9 | def __getitem__(self, key):
10 | return self._config_dist[key]
11 |
12 | __getattr__ = __getitem__
13 |
14 | __config = _Config('config.p')
15 | sys.modules[__name__] = __config
16 |
--------------------------------------------------------------------------------
/linot/services/__init__.py:
--------------------------------------------------------------------------------
1 | import pkgutil
2 | __all__ = []
3 | pkg_list = {}
4 | for loader, module_name, is_pkg in pkgutil.iter_modules(__path__):
5 | if is_pkg:
6 | __all__.append(module_name)
7 | module = loader.find_module(module_name).load_module(module_name)
8 | exec('%s = module' % module_name)
9 | __import__(module_name + '.service')
10 | pkg_list[module_name] = module.service
11 |
--------------------------------------------------------------------------------
/generate_config.py:
--------------------------------------------------------------------------------
1 | import pickle
2 | if __name__ == '__main__':
3 | LinotConfig = {
4 | 'interface': {
5 | 'line': {
6 | 'account': 'line_account',
7 | 'password': 'line_password',
8 | 'admin_id': 'line_admin_id',
9 | 'comp_name': 'LinotMaster',
10 | },
11 | },
12 | 'service': {
13 | 'twitch': {
14 | 'oauth': 'twitch_bot_account_oauth',
15 | 'user': 'twitch_bot_account_name',
16 | },
17 | },
18 | }
19 | pickle.dump(LinotConfig, open("config.p", "wb"))
20 |
--------------------------------------------------------------------------------
/linot/command_submitter.py:
--------------------------------------------------------------------------------
1 | import interfaces
2 |
3 |
4 | class CommandSubmitter:
5 | def __init__(self, if_name, code):
6 | self.interface_name = if_name
7 | self.code = code
8 |
9 | def send_message(self, msg):
10 | interface = interfaces.get(self.interface_name)
11 | return interface.send_message(self, msg)
12 |
13 | def get_display_name(self):
14 | interface = interfaces.get(self.interface_name)
15 | return interface.get_display_name(self)
16 |
17 | def __unicode__(self):
18 | return unicode(self.get_display_name())
19 |
20 | def __eq__(self, other):
21 | return hash(self) == hash(other)
22 |
23 | def __hash__(self):
24 | return hash(str(self.interface_name) + str(self.code))
25 |
--------------------------------------------------------------------------------
/tests/test_base_interface.py:
--------------------------------------------------------------------------------
1 | from nose.tools import ok_
2 | from nose.tools import raises
3 |
4 | from linot.base_interface import BaseInterface
5 |
6 |
7 | class TestBaseInterface:
8 | def setUp(self):
9 | self.interface = BaseInterface()
10 |
11 | def test_name(self):
12 | ok_(self.interface.NAME is None)
13 |
14 | @raises(NotImplementedError)
15 | def test_polling_command(self):
16 | self.interface.polling_command()
17 |
18 | @raises(NotImplementedError)
19 | def test_send_message(self):
20 | self.interface.send_message(None, 'test')
21 |
22 | @raises(NotImplementedError)
23 | def test_get_display_name(self):
24 | self.interface.get_display_name(None)
25 |
26 | @raises(ValueError)
27 | def test_sublcass_no_name(self):
28 | class ErrorSubClass(BaseInterface):
29 | pass
30 |
--------------------------------------------------------------------------------
/linot/base_interface.py:
--------------------------------------------------------------------------------
1 | class AttrEnforcer(type):
2 | def __init__(cls, name, bases, attrs):
3 | chk_list = ['NAME', 'SERVER']
4 | for attr in chk_list:
5 | if attr not in attrs:
6 | raise ValueError("Interface class: '{}' doesn't have {} attribute.".format(name, attr))
7 | type.__init__(cls, name, bases, attrs)
8 |
9 |
10 | class BaseInterface:
11 | """Subclass must defined NAME class variable"""
12 | __metaclass__ = AttrEnforcer
13 | NAME = None
14 | SERVER = True
15 |
16 | def polling_command(self):
17 | """Blocking polling command input"""
18 | raise NotImplementedError
19 | # submitter = CommandSubmitter(self.name, id)
20 | # command = ''
21 | # return [(submitter, command)]
22 |
23 | def send_message(self, receiver, msg):
24 | """Sends message to the receiver"""
25 | raise NotImplementedError
26 | # return True
27 |
28 | def get_display_name(self, submitter):
29 | """Get submitter display name"""
30 | raise NotImplementedError
31 | # return ''
32 |
--------------------------------------------------------------------------------
/linot/logger.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import logging
3 | import logging.config
4 |
5 | config = {
6 | 'version': 1,
7 | 'handlers': {
8 | 'console': {
9 | 'level': logging.DEBUG,
10 | 'class': 'logging.StreamHandler',
11 | 'stream': sys.stdout,
12 | 'formatter': 'verbose',
13 | },
14 | 'file': {
15 | 'level': logging.DEBUG,
16 | 'class': 'logging.handlers.TimedRotatingFileHandler',
17 | 'filename': 'linot.log',
18 | 'when': 'midnight',
19 | 'interval': 1,
20 | 'backupCount': 3,
21 | 'formatter': 'verbose',
22 | },
23 | },
24 | 'formatters': {
25 | 'verbose': {
26 | 'format': '%(asctime)s|[%(levelname)s][%(name)s]: %(message)s',
27 | 'datefmt': '%Y-%m-%d|%H:%M:%S',
28 | },
29 | },
30 | 'loggers': {
31 | '': {
32 | 'handlers': ['console', 'file'],
33 | 'level': logging.NOTSET
34 | },
35 | },
36 |
37 | }
38 | logging.config.dictConfig(config)
39 | logging.getLogger('urllib3').setLevel(logging.WARNING)
40 | logging.getLogger('requests').setLevel(logging.WARNING)
41 |
42 |
43 | def get():
44 | return logging
45 | # sys.modules[__name__] = logging
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Chun-Kai Chou and all contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/linot/interfaces/__init__.py:
--------------------------------------------------------------------------------
1 | import pkgutil
2 | import os
3 | import inspect
4 | import sys
5 |
6 | from linot.base_interface import BaseInterface
7 | from linot import logger
8 | logger = logger.get().getLogger(__name__)
9 |
10 | class_dict = {}
11 | instance_dict = {}
12 |
13 |
14 | def get(key):
15 | if key in class_dict and key not in instance_dict:
16 | instance_dict[key] = class_dict[key]()
17 | return instance_dict[key]
18 |
19 |
20 | def avail():
21 | return class_dict.keys()
22 |
23 |
24 | def find_and_import_interface_class(ifmod):
25 | for name, obj in inspect.getmembers(ifmod):
26 | if inspect.isclass(obj) and issubclass(obj, BaseInterface) and obj.NAME is not None:
27 | logger.debug('Found interface class: ' + str(obj))
28 | if obj.NAME == 'test' and 'nose' not in sys.modules: # pragma: no cover
29 | continue # skip test interface if we are not running test sutie
30 |
31 | if obj.NAME in class_dict:
32 | raise NameError('Interface name conflict: '.format(class_dict[obj.NAME], obj))
33 | else:
34 | class_dict[obj.NAME] = obj
35 |
36 | interface_folder = os.path.dirname(os.path.abspath(__file__))
37 | for importer, mod_name, _ in pkgutil.iter_modules([interface_folder]):
38 | if mod_name.endswith('interface'):
39 | ifmod = importer.find_module(mod_name).load_module(mod_name)
40 | find_and_import_interface_class(ifmod)
41 |
--------------------------------------------------------------------------------
/tests/test_command_submitter.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from nose.tools import ok_
4 |
5 | from linot.command_submitter import CommandSubmitter
6 | import linot.interfaces as interfaces
7 |
8 |
9 | class TestCommandSubmitter:
10 | def setUp(self):
11 | self.sender = CommandSubmitter('test', 'sender')
12 | interfaces.get('test').reset()
13 | self.interface = interfaces.get('test')
14 |
15 | def test_code(self):
16 | ok_(self.sender.code == 'sender')
17 |
18 | def test_send_message(self):
19 | test_msg = 'test message'
20 | self.sender.send_message(test_msg)
21 | ok_(len(self.interface.msg_queue[self.sender.code]) == 1)
22 | ok_(self.interface.msg_queue[self.sender.code][0] == test_msg,
23 | self.interface.msg_queue)
24 |
25 | def test_get_display_name(self):
26 | disp_name = self.sender.get_display_name()
27 | ok_(disp_name == self.interface.get_display_name(self.sender))
28 |
29 | def test_unicode(self):
30 | u_name = unicode(self.sender)
31 | ok_(u_name == unicode(self.sender.get_display_name()))
32 |
33 | def test_dict_hashable(self):
34 | sender_2 = CommandSubmitter('test', 'sender2')
35 | sender_3 = CommandSubmitter('some', 'sender')
36 | sender_same = CommandSubmitter('test', 'sender')
37 | some_dict = defaultdict(lambda: False)
38 | some_dict[self.sender] = True
39 | ok_(some_dict[self.sender] is True)
40 | ok_(some_dict[sender_2] is False)
41 | ok_(some_dict[sender_3] is False)
42 | ok_(some_dict[sender_same] is True)
43 |
--------------------------------------------------------------------------------
/linot/interfaces/test_interface.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Example interface implementation for testing"""
3 | from collections import defaultdict
4 |
5 | from linot.base_interface import BaseInterface
6 |
7 |
8 | class TestInterface(BaseInterface):
9 | NAME = 'test'
10 | SERVER = True
11 |
12 | def __init__(self):
13 | self.reset()
14 |
15 | def polling_command(self):
16 | for sender, cmd in self.command_queue:
17 | if self.polling_callback is not None:
18 | self.polling_callback()
19 | yield sender, cmd
20 | self.command_queue = []
21 |
22 | def send_message(self, receiver, msg):
23 | # sending zero length string to some interface may cause error
24 | # we check them here while runnig test suite
25 | assert len(msg) > 0
26 |
27 | self.msg_queue[receiver.code].append(msg) # compatibility
28 | self.msg_queue[receiver].append(msg)
29 | return True
30 |
31 | def get_display_name(self, submitter):
32 | name = '<{}>{}'.format(self.NAME, submitter.code)
33 | return name
34 |
35 | def reset(self):
36 | self.msg_queue = defaultdict(list)
37 | self.command_queue = []
38 | self.polling_callback = None
39 |
40 | def add_command(self, sender, cmd):
41 | assert sender.interface_name == self.NAME
42 | self.command_queue.append((sender, cmd))
43 |
44 | def add_command_list(self, cmd_list):
45 | for sender, cmd in cmd_list:
46 | self.add_command(sender, cmd)
47 |
48 | def set_polling_commad_callback(self, func):
49 | self.polling_callback = func
50 |
--------------------------------------------------------------------------------
/linot/services/service_base.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from linot.arg_parser import LinotArgParser
4 |
5 |
6 | class AttrEnforcer(type):
7 | def __init__(cls, name, bases, attrs):
8 | chk_list = ['CMD']
9 | for attr in chk_list:
10 | if attr not in attrs:
11 | raise ValueError("Interface class: '{}' doesn't have {} attribute.".format(name, attr))
12 | type.__init__(cls, name, bases, attrs)
13 |
14 |
15 | class ServiceBase:
16 | __metaclass__ = AttrEnforcer
17 | CMD = 'test'
18 |
19 | def __init__(self):
20 | self._started = False
21 |
22 | def __str__(self):
23 | return '{} ({})'.format(sys.modules[self.__module__].__package__, self.CMD)
24 |
25 | def setup(self, parser):
26 | ap = LinotArgParser(self.CMD, parser, self._cmd_process)
27 | self._setup_argument(ap)
28 |
29 | def is_start(self):
30 | return self._started
31 |
32 | def start(self):
33 | if not self._started:
34 | self._start()
35 | self._started = True
36 |
37 | def stop(self):
38 | if self._started:
39 | self._stop()
40 | self._started = False
41 |
42 | # Plugin should be designed to be safely stopped and re-started at anytime
43 | def _start(self):
44 | # Plugin start working!
45 | raise NotImplementedError
46 |
47 | def _stop(self):
48 | # Plugin stops
49 | raise NotImplementedError
50 |
51 | def _setup_argument(self, cmd_group):
52 | # Add the plugin specific arguments
53 | raise NotImplementedError
54 |
55 | def _cmd_process(self, args, sender):
56 | # process argument input
57 | if args is None:
58 | # no known arguments
59 | sender.send_message('Unknown commands.')
60 | else:
61 | sender.send_message('Command is not implemented yet')
62 |
--------------------------------------------------------------------------------
/linot/linot.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import argparse
3 | import io
4 |
5 | import services
6 | import config
7 | import command_server
8 | from arg_parser import LinotArgParser, LinotParser
9 | import logger
10 | logger = logger.get().getLogger(__name__)
11 |
12 |
13 | service_instances = {}
14 |
15 |
16 | def cmd_process(args, sender):
17 | if args.stopserver:
18 | if sender.code == config['interface'][sender.interface_name]['admin_id']:
19 | sender.send_message('Server is shutting down')
20 | logger.info('server is shutting down')
21 | for service in service_instances:
22 | logger.debug('stopping service: ' + service)
23 | service_instances[service].stop()
24 | logger.debug(service + ' is stopped')
25 | logger.debug('stopping command server')
26 | command_server.stop()
27 | return
28 |
29 | if args.listservices:
30 | msg = io.BytesIO()
31 | for service in service_instances:
32 | print(service_instances[service], file=msg)
33 | sender.send_message(msg.getvalue())
34 | return
35 |
36 |
37 | def main():
38 | # Add common commands
39 | parser = LinotParser(usage=argparse.SUPPRESS, add_help=False)
40 | cmd_group = LinotArgParser('linot', parser, cmd_process)
41 | cmd_group.add_argument('-stopserver', action='store_true', help=argparse.SUPPRESS)
42 | cmd_group.add_argument('-listservices', action='store_true', help='Show installed services')
43 | # cmd_group.add_argument('-backup', action='store_true', help=argparse.SUPPRESS)
44 | # cmd_group.add_argument('-listbackups', action='store_true', help=argparse.SUPPRESS)
45 | # cmd_group.add_argument('-restore', help=argparse.SUPPRESS)
46 |
47 | # Load plugins
48 | for service in services.pkg_list:
49 | logger.info('Loading service: ' + service)
50 | service_instance = services.pkg_list[service].Service()
51 | service_instance.setup(parser)
52 | service_instances[service] = service_instance
53 | service_instance.start()
54 |
55 | command_server.start(parser)
56 |
57 | if __name__ == '__main__':
58 | main()
59 |
--------------------------------------------------------------------------------
/tests/interfaces/test_interfaces.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import sys
4 |
5 | from nose.tools import ok_
6 |
7 | from linot import interfaces
8 |
9 |
10 | def create_dummy_interface(file_name, interface_name='delme', server=True):
11 | path = os.path.dirname(interfaces.__file__)
12 | with open(os.path.join(path, file_name), 'w+') as f:
13 | print('from linot.base_interface import BaseInterface', file=f)
14 | print('class DelMeInterface(BaseInterface):', file=f)
15 | print(" NAME='{}'".format(interface_name), file=f)
16 | print(' SERVER={}'.format(server), file=f)
17 | f.flush()
18 |
19 |
20 | class TestIntrefaces:
21 | def test_import_only_ends_with_interface(self):
22 | path = os.path.dirname(interfaces.__file__)
23 | with open(os.path.join(path, 'del_me.py'), 'w+') as f:
24 | print('from nose.tools import ok_', file=f)
25 | print('ok_(False)', file=f)
26 | f.flush()
27 | reload(interfaces)
28 | ok_('interfaces.del_me' not in sys.modules)
29 | try:
30 | os.remove(os.path.join(path, 'del_me.py'))
31 | os.remove(os.path.join(path, 'del_me.pyc'))
32 | except OSError:
33 | pass
34 |
35 | create_dummy_interface(file_name='del_me_interface.py')
36 | loaded = False
37 | try:
38 | reload(interfaces)
39 | except ValueError:
40 | loaded = True
41 | os.remove(os.path.join(path, 'del_me_interface.py'))
42 | os.remove(os.path.join(path, 'del_me_interface.pyc'))
43 | if 'del_me_interface' in sys.modules:
44 | loaded = True
45 | ok_(loaded)
46 |
47 | def test_name_conflict(self):
48 | path = os.path.dirname(interfaces.__file__)
49 | create_dummy_interface(file_name='del_me_interface.py',
50 | interface_name='test')
51 |
52 | error = False
53 | try:
54 | reload(interfaces)
55 | except NameError:
56 | error = True
57 | os.remove(os.path.join(path, 'del_me_interface.py'))
58 | os.remove(os.path.join(path, 'del_me_interface.pyc'))
59 | ok_(error)
60 | reload(interfaces) # clean up mess
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linot
2 | [](https://gitter.im/KavenC/Linot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3 |
4 | Linot is a chat bot for [LINE](http://line.me/)™ App. It provides services to user through the LINE message interfaces.
5 |
6 | Contact: leave message on gitter or [twitter@nevak](https://twitter.com/nevak)
7 |
8 | ## Update
9 | **2015-08-29** Python LINE API package has been pulled down from Github by official request from Naver. It should be a clear sign that the owner of LINE (a.k.a Naver Corps.) is not allowing custom LINE chat bot being made and run. In response to current situation, I decide to halt Linot development until I find another communication interface (perhaps Whatsapp?).
10 |
11 | Any code contributions are still always welcomed.
12 |
13 | ## Services
14 | ### TwitchNotifier
15 | TwitchNotifier let user subscribe twitch channels. When the subscribed channels goes live, Linot sends a LINE message to the subscribers with the channel infomation.
16 | * Example
17 | ```
18 | User> twitch -subscribe Nightblue3
19 | Linot> Done.
20 | (When Nightblue3 goes live)
21 | Linot>
22 | Nightblue3 is now streamming!!
23 | [Title] ASSASSIN/SATED JUNGLE META
24 | [Playing] League of Legends
25 | http://www.twitch.tv/nightblue3
26 | ```
27 | * Command List
28 | ```
29 | User> twitch -h
30 | ```
31 |
32 | ## Installation & Run
33 | 1. This bot is run under **Python2.7**. You will also need [LINE API](http://carpedm20.github.io/line/) package.
34 | 2. Please notice that there is currently an issue on the [thrift](https://github.com/apache/thrift) which may randomly connection lost when using LINE API. Please refere to this [thread](https://github.com/carpedm20/LINE/issues/9).
35 | 3. Fill out generate_config.py and generate the config file.
36 | - Since it is a LINE chat bot, you need to provide a valid LINE account/password.
37 | - If you are going to load TwitchNotifier, you need to provide a oauth key to a twitch account.
38 | 4. To run the bot: `python run.py`
39 | * You may need to enter the pompt code to the LINE app on your cellphone to complete the authentication process.
40 |
41 | ## Run Tests
42 | 1. We use nose to write test cases, make sure you have nose installed. ex: `pip install nose`
43 | 2. Run test cases by: `nosetests -c nose.cfg`
44 |
--------------------------------------------------------------------------------
/tests/services/test_service_base.py:
--------------------------------------------------------------------------------
1 | from nose.tools import ok_
2 | from nose.tools import raises
3 |
4 | from linot import interfaces
5 | from linot.services.service_base import ServiceBase
6 | from linot.command_submitter import CommandSubmitter
7 | from linot.arg_parser import LinotParser
8 |
9 |
10 | class TestServiceBase:
11 | def setUp(self):
12 | self.service = ServiceBase()
13 |
14 | def test_init(self):
15 | ok_(not self.service.is_start())
16 |
17 | @raises(NotImplementedError)
18 | def test_setup(self):
19 | parser = LinotParser()
20 | self.service.setup(parser)
21 |
22 | @raises(NotImplementedError)
23 | def test_start(self):
24 | ok_(self.service.is_start() is False)
25 | self.service.start()
26 |
27 | def test_start_normal(self):
28 | def mock_start():
29 | pass
30 | self.service._start = mock_start
31 | self.service.start()
32 | ok_(self.service.is_start() is True)
33 | self.service = ServiceBase()
34 | self.service._started = True
35 | self.service.start() # should not have exception
36 |
37 | @raises(NotImplementedError)
38 | def test_stop(self):
39 | self.service._started = True
40 | self.service.stop()
41 |
42 | def test_stop_normal(self):
43 | def mock_stop():
44 | pass
45 | self.service.stop()
46 | self.service._started = True
47 | self.service._stop = mock_stop
48 | self.service.stop()
49 | ok_(self.service.is_start() is False)
50 |
51 | def test_cmd_process(self):
52 | test_sender = CommandSubmitter('test', 'mock')
53 | self.service._cmd_process(None, test_sender)
54 | test_if = interfaces.get('test')
55 | # cmd_process arg=None indicates the msg contains no recognized command
56 | # we should return something to let user know
57 | ok_('Unknown' in test_if.msg_queue[test_sender.code][0])
58 | test_if.reset()
59 | self.service._cmd_process('test', test_sender)
60 | ok_('not implemented' in test_if.msg_queue[test_sender.code][0])
61 |
62 | def test_str(self):
63 | str_trans = str(self.service)
64 | ok_(self.service.CMD in str_trans)
65 |
66 | @raises(ValueError)
67 | def test_attr_enforce(self):
68 | class ErrorService(ServiceBase):
69 | pass
70 |
--------------------------------------------------------------------------------
/linot/interfaces/line_interface.py:
--------------------------------------------------------------------------------
1 | import io
2 | import sys
3 | from threading import Lock
4 |
5 | from line import LineClient
6 | from line import LineContact
7 |
8 | from linot import config
9 | from linot import logger
10 | from linot.base_interface import BaseInterface
11 | from linot.command_submitter import CommandSubmitter
12 | logger = logger.get().getLogger(__name__)
13 |
14 |
15 | class LineClientP(LineClient):
16 | """Patch for Line package cert issue"""
17 |
18 | def __init__(self, acc, pwd):
19 | super(LineClientP, self).__init__(acc, pwd, com_name="LinotMaster")
20 | self.lock = Lock()
21 |
22 | def ready(self):
23 | f = open(self.CERT_FILE, 'r')
24 | self.certificate = f.read()
25 | f.close()
26 | return
27 |
28 | def find_contact_by_id(self, userid):
29 | contacts = self._getContacts([userid])
30 | if len(contacts) == 0:
31 | raise ValueError('getContacts from server failed, id:' + str(userid))
32 | return LineContact(self, contacts[0])
33 |
34 |
35 | class LineInterface(BaseInterface):
36 | NAME = 'line'
37 | SERVER = True
38 |
39 | def __init__(self):
40 | self._client = LineClientP(
41 | config['interface']['line']['account'],
42 | config['interface']['line']['password']
43 | )
44 | logger.debug('log-in done.')
45 | self._client.updateAuthToken()
46 | logger.debug('update auth done.')
47 |
48 | def polling_command(self):
49 | # hide longPoll debug msg
50 | org_stdout = sys.stdout
51 | sys.stdout = io.BytesIO()
52 | self._client.lock.acquire(True)
53 | ge = self._client.longPoll()
54 | op_list = []
55 | for op in ge:
56 | op_list.append(op)
57 | self._client.lock.release()
58 | sys.stdout = org_stdout
59 |
60 | # construct formal command list
61 | command_list = []
62 | for op in op_list:
63 | submitter = CommandSubmitter(self.NAME, op[0].id)
64 | command_list.append((submitter, op[2].text))
65 | return command_list
66 |
67 | def send_message(self, receiver, msg):
68 | assert receiver.interface_name == self.NAME
69 | self._send_message_to_id(receiver.code, msg)
70 |
71 | def get_display_name(self, submitter):
72 | contact = self._get_contact_by_id(submitter.code)
73 | return contact.name
74 |
75 | def _get_contact_by_id(self, id):
76 | self._client.lock.acquire(True)
77 | contact = self._client.find_contact_by_id(id)
78 | self._client.lock.release()
79 | return contact
80 |
81 | def _send_message_to_id(self, recvr_id, msg):
82 | recvr = self._get_contact_by_id(recvr_id)
83 | self._client.lock.acquire(True)
84 | recvr.sendMessage(msg)
85 | self._client.lock.release()
86 |
--------------------------------------------------------------------------------
/tests/interfaces/test_line_interface.py:
--------------------------------------------------------------------------------
1 | from nose.tools import ok_, raises
2 |
3 | from linot import config
4 | from linot.interfaces.line_interface import LineClientP, LineInterface
5 |
6 |
7 | class TestLineClientP:
8 | def setUp(self):
9 | self.line_cfg = config['interface']['line']
10 | self.lineclient = LineClientP(self.line_cfg['account'],
11 | self.line_cfg['password'])
12 |
13 | def test_find_contact_by_id(self):
14 | contact = self.lineclient.find_contact_by_id(self.line_cfg['admin_id'])
15 | ok_(contact.id == self.line_cfg['admin_id'])
16 |
17 | @raises(ValueError)
18 | def test_find_contact_by_id_exception(self):
19 | self.lineclient.find_contact_by_id(self.line_cfg['admin_id'][:-2])
20 |
21 |
22 | class TestLineInterface:
23 | def setUp(self):
24 | self.line_interface = LineInterface()
25 |
26 | def test_polling_command(self):
27 | test_str = 'testing longPoll correctness'
28 | # first sends a message to myself..
29 | me = self.line_interface._client.getProfile()
30 | me.sendMessage(test_str)
31 | result = self.line_interface.polling_command()
32 | ok_(len(result) == 1, result)
33 | submitter, msg = result[0]
34 | ok_(submitter.code == me.id, submitter)
35 | ok_(msg == test_str,
36 | 'Message context not match: {} <-> {}'.format(msg, test_str))
37 |
38 | def test_get_contact_by_id(self):
39 | me = self.line_interface._client.getProfile()
40 | contact = self.line_interface._get_contact_by_id(me.id)
41 | ok_(me.id == contact.id, '{} <-> {}'.format(me.id, contact.id))
42 |
43 | def test_send_message(self):
44 | # first sends a message to myself..
45 | test_str = 'testing send to client'
46 | me = self.line_interface._client.getProfile()
47 | me.sendMessage(test_str)
48 | result = self.line_interface.polling_command()
49 | me, msg = result[0]
50 | self.line_interface.send_message(me, test_str)
51 | result = self.line_interface.polling_command()
52 | me, msg = result[0]
53 | ok_(msg == test_str, 'message not match {} <-> {}'.format(msg, test_str))
54 |
55 | def test_send_message_to_id(self):
56 | # first sends a message to myself..
57 | test_str = 'testing send to id'
58 | me = self.line_interface._client.getProfile()
59 | me.sendMessage(test_str)
60 | result = self.line_interface.polling_command()
61 | me, msg = result[0]
62 | self.line_interface._send_message_to_id(me.code, test_str)
63 | result = self.line_interface.polling_command()
64 | me, msg = result[0]
65 | ok_(msg == test_str, 'message not match {} <-> {}'.format(msg, test_str))
66 |
67 | def test_get_display_name(self):
68 | # first sends a message to myself..
69 | test_str = 'testing send to id'
70 | me = self.line_interface._client.getProfile()
71 | me.sendMessage(test_str)
72 | result = self.line_interface.polling_command()
73 | me_submitter, msg = result[0]
74 | me_display_name = self.line_interface.get_display_name(me_submitter)
75 | ok_(me_display_name == me.name)
76 |
--------------------------------------------------------------------------------
/linot/command_server.py:
--------------------------------------------------------------------------------
1 | from threading import Thread, Event
2 |
3 | import interfaces
4 | import logger
5 | logger = logger.get().getLogger(__name__)
6 |
7 |
8 | class CmdServer(Thread):
9 | def __init__(self, cmd_parser, interface, response_wait=.5):
10 | super(CmdServer, self).__init__()
11 | self._stopped = True
12 | self._stop = Event()
13 | self._cmd_parser = cmd_parser
14 | self._response_wait = response_wait
15 | self._interface = interface
16 | self._logger = logger.getChild(interface.NAME)
17 |
18 | def run(self):
19 | self._stopped = False
20 | self._logger.info('server thread start')
21 | while(not self._stop.is_set()):
22 | cmd_list = self._interface.polling_command()
23 | for submitter, cmd in cmd_list:
24 | worker = Thread(target=self._process_command, args=(cmd, submitter))
25 | worker.start()
26 | self._stop.wait(self._response_wait)
27 |
28 | # thread stop process
29 | self._stopped = True
30 | self._logger.info('Thread stop')
31 |
32 | def _process_command(self, cmd, sender):
33 | # this function is runned by a worker thread
34 | logger = self._logger.getChild('worker')
35 | try:
36 | arg_list = cmd.split()
37 | logger.debug('get cmd: ' + str(arg_list))
38 | args, unknown_args = self._cmd_parser.parse_known_args(arg_list)
39 | if len(unknown_args) > 0:
40 | logger.debug('unknown args: ' + str(unknown_args)) # pragma: no cover
41 | args.proc(args, sender)
42 | except SystemExit as e:
43 | # TODO maybe these processes could be hided in to cmd parser
44 | if e.code == 2:
45 | # reach here if no sub command is found in the cmd
46 | # direct command is processed here
47 | matched = self._cmd_parser.process_direct_commands(cmd, sender)
48 | if not matched:
49 | # if no direct command is matching
50 | # response to user that we cannot recognize the command
51 | logger.debug('no known args found.')
52 | sender.send_message('Unknown commands.')
53 | else:
54 | logger.exception('Unexpected SystemExit') # pragma: no cover
55 |
56 | def async_stop(self):
57 | logger.debug('stop is called')
58 | self._stop.set()
59 |
60 | def stop(self):
61 | self.async_stop()
62 | logger.debug('waiting for thread end')
63 | self.join()
64 |
65 | server_threads = []
66 |
67 |
68 | def start(parser, iflist=interfaces.avail()):
69 | # starts one thread for each interface
70 | # CmdServer automatically starts a new server thread when received a new
71 | # command
72 | global server_threads
73 | server_threads = [] # if restarted, clear all old threads
74 | server_if_list = [x for x in iflist if interfaces.class_dict[x].SERVER]
75 | for interface in server_if_list:
76 | thread = CmdServer(parser, interfaces.get(interface))
77 | server_threads.append(thread)
78 | thread.start()
79 |
80 |
81 | def stop():
82 | for thread in server_threads:
83 | thread.async_stop()
84 |
85 | for thread in server_threads:
86 | thread.join()
87 |
--------------------------------------------------------------------------------
/linot/services/twitch_notifier/twitch_engine.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import print_function
4 | import json
5 | import requests
6 | import sys
7 |
8 | import linot.config as config
9 | import linot.logger
10 | logger = linot.logger.get().getLogger(__name__)
11 |
12 |
13 | class TwitchRequests: # pragma: no cover
14 | TWITCH_API_BASE = 'https://api.twitch.tv/kraken/'
15 | OAUTH_TOKEN = config['service']['twitch']['oauth']
16 | IGNORE_STATUS = [
17 | 204,
18 | 422,
19 | 404
20 | ]
21 |
22 | @classmethod
23 | def _twitch_process(cls, action, url, pms, **kwargs):
24 | twitch_api_url = cls.TWITCH_API_BASE + url
25 | if pms is None:
26 | pms_a = {}
27 | else:
28 | pms_a = pms
29 | pms_a['oauth_token'] = cls.OAUTH_TOKEN
30 | logger.debug('[{}] Url = {}'.format(str(action.__name__), str(twitch_api_url)))
31 | ret = action(twitch_api_url, params=pms_a, **kwargs)
32 | logger.debug('Return Code = {}'.format(ret.status_code))
33 | if ret.status_code not in cls.IGNORE_STATUS:
34 | return ret.json()
35 | else:
36 | return {'code': ret.status_code}
37 |
38 | @classmethod
39 | def get(cls, url, params=None, **kwargs):
40 | return cls._twitch_process(requests.get, url, params, **kwargs)
41 |
42 | @classmethod
43 | def multi_get(cls, url, params=None, per=25, **kwargs):
44 | if params is None:
45 | params = {
46 | 'limit': per,
47 | 'offset': 0
48 | }
49 | else:
50 | params['limit'] = per
51 | params['offset'] = 0
52 | json_streams = cls.get(url, params=params, **kwargs)
53 | resp_list = [json_streams]
54 | if '_total' in json_streams:
55 | total = json_streams['_total']
56 | for offset in range(per, total, per):
57 | params['offset'] = offset
58 | params['limit'] = offset + per
59 | json_streams = cls.get(url, params=params, **kwargs)
60 | resp_list.append(json_streams)
61 | return resp_list
62 |
63 | @classmethod
64 | def put(cls, url, params=None, **kwargs):
65 | return cls._twitch_process(requests.put, url, params, **kwargs)
66 |
67 | @classmethod
68 | def delete(cls, url, params=None, **kwargs):
69 | return cls._twitch_process(requests.delete, url, params, **kwargs)
70 |
71 | @classmethod
72 | def post(cls, url, params=None, **kwargs):
73 | return cls._twitch_process(requests.post, url, params, **kwargs)
74 |
75 |
76 | def JSONPrint(dic): # pragma: no cover
77 | print(json.dumps(dic, indent=2, separators=(',', ':')), file=sys.stderr)
78 |
79 |
80 | class TwitchEngine:
81 |
82 | USER = config['service']['twitch']['user']
83 |
84 | def get_followed_channels(self, user):
85 | json_channels_list = TwitchRequests.multi_get('/users/' + user + '/follows/channels')
86 |
87 | channels = {}
88 | for json_channels in json_channels_list:
89 | # user not found
90 | if 'code' in json_channels and json_channels['code'] == 404:
91 | return None
92 |
93 | for followed_channel in json_channels['follows']:
94 | name = followed_channel['channel']['display_name']
95 | channels[name] = followed_channel['channel']
96 | return channels
97 |
98 | def get_live_channels(self):
99 | live_channel_json = TwitchRequests.multi_get('/streams/followed')
100 | ret_live_channels = {}
101 | for json_streams in live_channel_json:
102 | for stream in json_streams['streams']:
103 | name = stream['channel']['display_name']
104 | ret_live_channels[name] = stream['channel']
105 | return ret_live_channels
106 |
107 | def follow_channel(self, channel_name):
108 | json_streams = TwitchRequests.put(
109 | '/users/' + self.USER + '/follows/channels/' + channel_name)
110 | if 'channel' not in json_streams:
111 | return channel_name, False
112 | else:
113 | return json_streams['channel']['display_name'], True
114 |
115 | def unfollow_channel(self, channel_name):
116 | json_streams = TwitchRequests.delete(
117 | '/users/' + self.USER + '/follows/channels/' + channel_name)
118 | if json_streams['code'] == 204:
119 | return True
120 | else:
121 | return False
122 |
123 | def get_channel_info(self, channel):
124 | json_streams = TwitchRequests.get('/channels/{}/'.format(channel))
125 | if 'display_name' not in json_streams:
126 | return None
127 | else:
128 | return json_streams
129 |
--------------------------------------------------------------------------------
/linot/arg_parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Argument Parser for Linot input commands
3 |
4 | This modules rewrites or extends functions of `argparse` in the Python
5 | Starndard Library. It simplifies the command interface so that services
6 | can be developed without worrying about the complexity of user inputs
7 |
8 | """
9 |
10 | from __future__ import print_function
11 | from argparse import SUPPRESS, ArgumentParser
12 | import re
13 | from io import BytesIO
14 |
15 | import logger
16 | logger = logger.get().getLogger(__name__)
17 |
18 |
19 | class LinotParser(ArgumentParser):
20 | """Extends the usibility of ArgumentParser
21 |
22 | Attributes:
23 | (same with ArgumentParser in standard library)
24 |
25 | """
26 | def __init__(self, *args, **kwargs):
27 | ArgumentParser.__init__(self, *args, **kwargs)
28 | self._sub_parser = None
29 | self._direct_commands = []
30 |
31 | def get_sub_parser(self):
32 | if self._sub_parser is None:
33 | self._sub_parser = self.add_subparsers()
34 | return self._sub_parser
35 |
36 | def add_direct_command(self, func, pattern, flags=0):
37 | self._direct_commands.append([func, pattern, flags])
38 |
39 | def process_direct_commands(self, cmd, sender):
40 | matched = False
41 | for direct_command in self._direct_commands:
42 | match_list = re.findall(direct_command[1], cmd, direct_command[2])
43 | if len(match_list) > 0:
44 | matched = True
45 | direct_command[0](match_list, cmd, sender)
46 | return matched
47 |
48 |
49 | class LinotArgParser:
50 | def __init__(self, subcmd, parser, default_process):
51 | self._parser = parser
52 | sub_cmd_parser = parser.get_sub_parser()
53 | ap = sub_cmd_parser.add_parser(subcmd, add_help=False)
54 | self._sub_parser = ap.add_mutually_exclusive_group()
55 | self._subcmd = subcmd
56 | self._default_process = default_process
57 | self._arg_list = {}
58 | self._sub_parser.set_defaults(proc=self._process_args)
59 | self.add_argument('-h', '--help', action='store_true', func=self.print_help, help='show command list')
60 |
61 | def add_direct_command(self, func, pattern, flags=0):
62 | # Add direct command parsing rule (re) and the callback function
63 | # if parser cannot find any known command in the input string
64 | # it starts match the pattern in the direct command list for input
65 | # All the funcs of the matching pattern will be called
66 | self._parser.add_direct_command(func, pattern, flags)
67 |
68 | def add_argument(self, *args, **kwargs):
69 | # Limit some funtions of argparse.add_argument
70 | # make argument bind with callback on add
71 | if 'dest' in kwargs:
72 | raise ValueError('"dest" is forbidden for plugin argument')
73 | if 'func' in kwargs:
74 | func = kwargs['func']
75 | kwargs.pop('func')
76 | else:
77 | func = None
78 | for arg_str in args:
79 | # we dont accept positional args
80 | if not arg_str.startswith(self._sub_parser.prefix_chars):
81 | raise ValueError('supports only optional args starts with ' +
82 | self._sub_parser.prefix_chars)
83 | option = arg_str.lstrip(self._sub_parser.prefix_chars)
84 | self._arg_list[option] = {}
85 | self._arg_list[option]['func'] = func
86 | self._arg_list[option]['arg'] = arg_str
87 | if 'help' in kwargs:
88 | self._arg_list[option]['help'] = kwargs['help']
89 | else:
90 | self._arg_list[option]['help'] = ''
91 | self._sub_parser.add_argument(*args, **kwargs)
92 |
93 | def _process_args(self, input_args, sender):
94 | for args in self._arg_list:
95 | value = getattr(input_args, args, None)
96 | if value is not None and value is not False:
97 | func = self._arg_list[args]['func']
98 | if func is not None:
99 | func(getattr(input_args, args), sender)
100 | else:
101 | self._default_process(input_args, sender)
102 | return # we assume that there will be only 1 argument each time
103 |
104 | # this call indicates that there are no known arguments
105 | # default process can handle this by determine if args is None
106 | self._default_process(None, sender)
107 |
108 | def print_help(self, args, sender=None):
109 | msg = BytesIO()
110 | print('[{} command list]'.format(self._subcmd), file=msg)
111 | # TODO fix this ....
112 | print('{} {}'.format(self._subcmd, '-h/--help'), file=msg)
113 | print('>> show this command list.', file=msg)
114 | print('--------------', file=msg)
115 | for arg in self._arg_list:
116 | if self._arg_list[arg]['help'] is SUPPRESS:
117 | continue
118 | if arg == 'help' or arg == 'h':
119 | continue
120 | help_text = self._arg_list[arg]['help']
121 | print('{} {}'.format(self._subcmd, self._arg_list[arg]['arg']), file=msg)
122 | if help_text != '':
123 | print('>> '+help_text, file=msg)
124 | print('--------------', file=msg)
125 | if sender is not None:
126 | sender.send_message(msg.getvalue())
127 |
--------------------------------------------------------------------------------
/tests/test_arg_parser.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import re
3 |
4 | from nose.tools import assert_raises
5 | from nose.tools import raises
6 | from nose.tools import ok_
7 | from nose.tools import eq_
8 |
9 | from linot.arg_parser import LinotArgParser, LinotParser
10 | import linot.interfaces as interfaces
11 | from linot.command_submitter import CommandSubmitter
12 |
13 |
14 | class TestLinotArgParser:
15 | def setUp(self):
16 | parser = LinotParser(usage=argparse.SUPPRESS, add_help=False)
17 | self.parser = parser
18 |
19 | def test_print_help(self):
20 | def cmd_process(args, sender):
21 | pass
22 |
23 | test_str = 'testtesttest'
24 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
25 | lap.add_argument('-a', action='store_true', help=test_str)
26 | lap.add_argument('-noshow', action='store_true', help=argparse.SUPPRESS)
27 | lap.add_argument('-showme', action='store_true', help='')
28 |
29 | # test -h and --help goes to print_help
30 | args, unknown_args = self.parser.parse_known_args('testcmd -h'.split())
31 | eq_(len(unknown_args), 0)
32 | sender = CommandSubmitter('test', 'test_sender')
33 | args.proc(args, sender)
34 | msg_queue = interfaces.get('test').msg_queue
35 | ok_('command list' in ' '.join(msg_queue[sender.code]))
36 | args, unknown_args = self.parser.parse_known_args('testcmd --help'.split())
37 | eq_(len(unknown_args), 0)
38 | interfaces.get('test').reset()
39 | args.proc(args, sender)
40 | msg_queue = interfaces.get('test').msg_queue
41 | ok_('command list' in ' '.join(msg_queue[sender.code]))
42 |
43 | # add help
44 | args, unknown_args = self.parser.parse_known_args('testcmd -h'.split())
45 | interfaces.get('test').reset()
46 | args.proc(args, sender)
47 | msg_queue = interfaces.get('test').msg_queue
48 | msg = ' '.join(msg_queue[sender.code])
49 | ok_(test_str in msg, msg)
50 | ok_('-nowshow' not in msg)
51 | ok_('-showme' in msg)
52 |
53 | # Test help suppress if sender not found (for coverage)
54 | args, unknown_args = self.parser.parse_known_args('testcmd -h'.split())
55 | interfaces.get('test').reset()
56 | args.proc(args, None)
57 | msg_queue = interfaces.get('test').msg_queue
58 | msg = ' '.join(msg_queue[sender.code])
59 | ok_(msg is '', msg)
60 |
61 | def test_add_argument_exclusiveness(self):
62 | def cmd_process(args, sender):
63 | ok_((args.a and args.b) is False)
64 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
65 | lap.add_argument('-a', action='store_true')
66 | lap.add_argument('-b', action='store_true')
67 | with assert_raises(SystemExit) as e:
68 | args, unknown_args = self.parser.parse_known_args('testcmd -a -b'.split())
69 | check_str = 'not allowed with'
70 | ok_(check_str in e.msg)
71 |
72 | def test_add_argument_dest_exception(self):
73 | def cmd_process(args, sender):
74 | pass
75 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
76 | with assert_raises(ValueError):
77 | lap.add_argument('-a', dest='b', action='store_true')
78 |
79 | def test_add_argument_func(self):
80 | def cmd_process(args, sender):
81 | ok_(args.b and not sender)
82 |
83 | def cust_func(value, sender):
84 | ok_(value and sender)
85 |
86 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
87 | lap.add_argument('-a', action='store_true', func=cust_func)
88 | lap.add_argument('-b', action='store_true') # default proc
89 | args, unknown_args = self.parser.parse_known_args('testcmd -a'.split())
90 | args.proc(args, True)
91 | args, unknown_args = self.parser.parse_known_args('testcmd -b'.split())
92 | args.proc(args, False)
93 |
94 | def test_add_argument_multiargs(self):
95 | def cmd_process(args, sender):
96 | ok_(False)
97 |
98 | def cust_func(value, sender):
99 | ok_(value)
100 | cust_func.called += 1
101 |
102 | cust_func.called = 0
103 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
104 | lap.add_argument('-a', '-b', '-c', action='store_true', func=cust_func)
105 | args, unkown_args = self.parser.parse_known_args('testcmd -a'.split())
106 | args.proc(args, None)
107 | args, unkown_args = self.parser.parse_known_args('testcmd -b'.split())
108 | args.proc(args, None)
109 | args, unkown_args = self.parser.parse_known_args('testcmd -c'.split())
110 | args.proc(args, None)
111 | ok_(cust_func.called == 3)
112 |
113 | @raises(ValueError)
114 | def test_add_argument_positional(self):
115 | def cmd_process(args, sender):
116 | ok_(False)
117 |
118 | lap = LinotArgParser('testcmd', self.parser, cmd_process)
119 | lap.add_argument('abc', action='store_true')
120 |
121 | def test_subcmd_default_process(self):
122 | def cmd_process(args, sender):
123 | cmd_process.called = True
124 | ok_(args is None)
125 | ok_(sender == 'test_sender')
126 |
127 | cmd_process.called = False
128 | LinotArgParser('testcmd', self.parser, cmd_process)
129 | args, unknown_args = self.parser.parse_known_args('testcmd'.split())
130 | args.proc(args, 'test_sender')
131 |
132 | def test_direct_command(self):
133 | def cmd_checker1(match_list, cmd, sender):
134 | cmd_checker1.runned = True
135 | cmd_checker1.cmd = cmd
136 | ok_('1234' in match_list)
137 |
138 | def cmd_checker2(match_list, cmd, sender):
139 | cmd_checker2.runned = True
140 | cmd_checker2.cmd = cmd
141 | ok_('1234' in match_list)
142 |
143 | # Here we only test the api correctness
144 | # the integration test will be in command_server test
145 | ap = LinotArgParser('testcmd', self.parser, None)
146 | ap.add_direct_command(cmd_checker1, '[cxyz]+([0-9]+)', re.IGNORECASE)
147 | ap = LinotArgParser('testcmd2', self.parser, None)
148 | ap.add_direct_command(cmd_checker2, '[abc]+([0-9]+)', re.IGNORECASE)
149 | cmd_checker1.runned = False
150 | cmd_checker2.runned = False
151 | self.parser.process_direct_commands('1234', None)
152 | ok_(cmd_checker1.runned is False)
153 | ok_(cmd_checker2.runned is False)
154 | self.parser.process_direct_commands('xyz1234', None)
155 | ok_(cmd_checker1.runned is True)
156 | ok_(cmd_checker2.runned is False)
157 | cmd_checker1.runned = False
158 | self.parser.process_direct_commands('ab1234', None)
159 | ok_(cmd_checker1.runned is False)
160 | ok_(cmd_checker2.runned is True)
161 | cmd_checker2.runned = False
162 | self.parser.process_direct_commands('c1234', None)
163 | ok_(cmd_checker1.runned is True)
164 | ok_(cmd_checker2.runned is True)
165 | ok_(cmd_checker1.cmd == 'c1234')
166 | ok_(cmd_checker2.cmd == 'c1234')
167 |
--------------------------------------------------------------------------------
/tests/test_command_server.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import re
3 | import argparse
4 |
5 | from nose.tools import ok_
6 | from nose.tools import timed
7 |
8 | from linot.arg_parser import LinotArgParser, LinotParser
9 | from linot import command_server
10 | import linot.interfaces as interfaces
11 | from linot.command_submitter import CommandSubmitter
12 |
13 |
14 | class fakeMessage:
15 | def __init__(self, msg):
16 | self.text = msg
17 |
18 |
19 | class mock_engine:
20 | def __init__(self):
21 | self.test_list = []
22 | self.test_case_idx = 0
23 | self.test_case_lock = threading.Lock()
24 | self.test_finished = threading.Event()
25 | self.test_finished.clear()
26 |
27 | def _case_done(self):
28 | self.test_case_lock.acquire(True)
29 | self.test_case_idx += 1
30 | self.test_case_lock.release()
31 |
32 | def addTest(self, cmd_list, cmd_checker, msg_checker):
33 | self.test_list.append([cmd_list, cmd_checker, msg_checker])
34 |
35 | def longPoll(self):
36 | self.test_case_lock.acquire(True)
37 | if self.test_case_idx < len(self.test_list):
38 | ret = self.test_list[self.test_case_idx][0]
39 | else:
40 | self.test_finished.set()
41 | ret = []
42 | self.test_case_lock.release()
43 | return ret
44 |
45 | def sendMessageToClient(self, recv, msg):
46 | ok_(self.test_case_idx < len(self.test_list))
47 | callback = self.test_list[self.test_case_idx][2]
48 | if callback is not None:
49 | callback(recv, msg, self._case_done)
50 |
51 | def cmdProcess(self, args, sender):
52 | ok_(self.test_case_idx < len(self.test_list))
53 | callback = self.test_list[self.test_case_idx][1]
54 | if callback is not None:
55 | callback(args, sender, self._case_done)
56 |
57 |
58 | class CmdTester:
59 | def __init__(self, test_if_name):
60 | self.if_name = test_if_name
61 | self.senders = []
62 | self.cmd_checkers = []
63 | self.msg_checkers = []
64 | self.total_test_count = 0
65 |
66 | def add_test(self, test_list):
67 | for sender_code, cmd, cmd_chk, msg_chk in test_list:
68 | sender = CommandSubmitter(sender_code, self.if_name)
69 | interfaces.get(self.in_name).add_command(sender, cmd)
70 | self.senders[self.total_test_count] = sender
71 | self.cmd_checkers[self.total_test_count] = cmd_chk
72 | self.msg_checkers[self.total_test_count] = msg_chk
73 | self.total_test_count += 1
74 |
75 | def cmd_process(self, args, sender):
76 | if self.cmd_checker is not None:
77 | self.cmd_checker(args, sender)
78 |
79 |
80 | class TestCmdServer:
81 | def setUp(self):
82 | parser = LinotParser(usage=argparse.SUPPRESS, add_help=False)
83 | self.parser = parser
84 |
85 | def test_cmd_process(self):
86 | # test command server can properly handle 1 or multiple commands
87 | def default_process(args, sender):
88 | ok_(getattr(args, sender.code) is True)
89 | default_process.called += 1
90 |
91 | lap = LinotArgParser('testcmd', self.parser, default_process)
92 | lap.add_argument('-a', action='store_true')
93 | lap.add_argument('-b', action='store_true')
94 | lap.add_argument('-c', action='store_true')
95 |
96 | # Test 1 cmd return by polling_command
97 | # command = [(sender, cmd string), ...]
98 | sender = CommandSubmitter('test', 'a')
99 | fake_cmd = [(sender, 'testcmd -a')]
100 | interfaces.get('test').add_command_list(fake_cmd)
101 | default_process.called = 0
102 | command_server.start(self.parser, ['test'])
103 | threading.Event().wait(.5)
104 | command_server.stop()
105 | ok_(default_process.called == 1)
106 |
107 | # Test 3 cmds return by polling_command
108 | # command = [(sender, cmd string), ...]
109 | sender_a = CommandSubmitter('test', 'a')
110 | sender_b = CommandSubmitter('test', 'b')
111 | sender_c = CommandSubmitter('test', 'c')
112 | fake_cmd = [
113 | (sender_a, 'testcmd -a'),
114 | (sender_b, 'testcmd -b'),
115 | (sender_c, 'testcmd -c'),
116 | ]
117 | interfaces.get('test').add_command_list(fake_cmd)
118 | command_server.start(self.parser, ['test'])
119 | default_process.called = 0
120 | threading.Event().wait(.5)
121 | command_server.stop()
122 | ok_(default_process.called == 3)
123 |
124 | @timed(10)
125 | def test_unknown_commands(self):
126 | # test command server can properly handle 1 or multiple commands
127 | def default_process(args, sender):
128 | ok_(getattr(args, sender.code) is True)
129 | default_process.called += 1
130 |
131 | lap = LinotArgParser('testcmd', self.parser, default_process)
132 | lap.add_argument('-a', action='store_true')
133 | lap.add_argument('-b', action='store_true')
134 | lap.add_argument('-c', action='store_true')
135 |
136 | # Test 1 unknown command, command server respose to user
137 | sender_a = CommandSubmitter('test', 'a')
138 | fake_cmd = [
139 | (sender_a, 'some_unknown_words'),
140 | ]
141 | interfaces.get('test').reset()
142 | interfaces.get('test').add_command_list(fake_cmd)
143 | command_server.start(self.parser, ['test'])
144 | default_process.called = 0
145 | threading.Event().wait(.5)
146 | command_server.stop()
147 | ok_(default_process.called == 0)
148 | ok_('Unknown' in ' '.join(interfaces.get('test').msg_queue[sender_a.code]))
149 |
150 | # Test multiple unknown commands
151 | sender_a = CommandSubmitter('test', 'a')
152 | sender_u = CommandSubmitter('test', 'u')
153 | fake_cmd = [
154 | (sender_u, 'some_unknown_cmds'),
155 | (sender_a, 'testcmd -a'),
156 | (sender_u, 'some_unknown_cmds'),
157 | ]
158 | interfaces.get('test').reset()
159 | interfaces.get('test').add_command_list(fake_cmd)
160 | command_server.start(self.parser, ['test'])
161 | default_process.called = 0
162 | threading.Event().wait(.5)
163 | command_server.stop()
164 | unknown_response = ' '.join(interfaces.get('test').msg_queue[sender_u.code])
165 | ok_(unknown_response.count('Unknown') == 2)
166 | ok_(default_process.called == 1)
167 |
168 | def test_direct_command(self):
169 | def default_process(args, sender):
170 | ok_(False) # should not reach here
171 |
172 | lap = LinotArgParser('testcmd', self.parser, default_process)
173 |
174 | def cmd_checker(match_list, cmd, sender):
175 | ok_('somechannel' in match_list)
176 | ok_(len(match_list) == 1)
177 | ok_(cmd == 'www.twitch.tv/somechannel')
178 | cmd_checker.runned = True
179 | lap.add_direct_command(cmd_checker, 'twitch\.tv/(\w+)[\s\t,]*', re.IGNORECASE)
180 | cmd_checker.runned = False
181 |
182 | sender = CommandSubmitter('test', 'sender')
183 | fake_cmd = [
184 | (sender, 'www.twitch.tv/somechannel')
185 | ]
186 | interfaces.get('test').add_command_list(fake_cmd)
187 | command_server.start(self.parser, ['test'])
188 | threading.Event().wait(.5)
189 | command_server.stop()
190 | ok_(cmd_checker.runned)
191 |
192 | def test_stop(self):
193 | command_server.start(self.parser)
194 | for server in command_server.server_threads:
195 | server.stop()
196 | ok_(server.is_alive() is not True)
197 |
--------------------------------------------------------------------------------
/linot/services/twitch_notifier/service.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from collections import defaultdict
3 | from threading import Thread, Event, Lock
4 | import pickle
5 | import argparse
6 | import re
7 | import copy
8 | import io
9 |
10 | from repoze.lru import LRUCache
11 |
12 | import linot.config as config
13 | from .twitch_engine import TwitchEngine
14 | from linot.services.service_base import ServiceBase
15 | import linot.logger
16 | logger = linot.logger.get().getLogger(__name__)
17 |
18 |
19 | class Checker(Thread):
20 | def __init__(self, period, twitch, get_sublist):
21 | super(Checker, self).__init__()
22 | self._stop = Event()
23 | self._polling = Event()
24 | self._period = period
25 | self._twitch = twitch
26 | self._get_sublist = get_sublist
27 | self._status_lock = Lock()
28 | self._logger = logger.getChild(self.__class__.__name__)
29 |
30 | def run(self):
31 | self._logger.info('Twitch Checker is started')
32 | # Skip 1st notify if channels are already live before plugin load
33 | self._set_live_channels(self._twitch.get_live_channels())
34 |
35 | while(not self._stop.is_set()):
36 | self._logger.debug('Wait polling {} sec.'.format(self._period))
37 | self._polling.wait(self._period)
38 | self._polling.clear()
39 |
40 | self._logger.debug('Try get live channels')
41 | current_live_channels = self._twitch.get_live_channels()
42 | local_live_channels = self.get_live_channels()
43 |
44 | off_channels = local_live_channels.viewkeys() - current_live_channels.viewkeys()
45 | for ch in off_channels:
46 | # TODO do we have to notify user the channel went off?
47 | del local_live_channels[ch]
48 |
49 | # Send live notifications to subcribers
50 | new_live_channels = current_live_channels.viewkeys() - local_live_channels.viewkeys()
51 | self._logger.debug('New live channels:' + str(new_live_channels))
52 | for ch in new_live_channels:
53 | local_live_channels[ch] = current_live_channels[ch]
54 | local_sublist = self._get_sublist()
55 | check_name = ch.lower()
56 | for user in local_sublist:
57 | if check_name in local_sublist[user]:
58 | msg = io.StringIO()
59 | print(u'{} is now streamming!!'.format(unicode(ch)), file=msg)
60 | print(u'[Title] {}'.format(unicode(current_live_channels[ch]['status'])), file=msg)
61 | print(u'[Playing] {}'.format(unicode(current_live_channels[ch]['game'])), file=msg)
62 | print(current_live_channels[ch]['url'], file=msg)
63 | user.send_message(msg.getvalue())
64 |
65 | self._set_live_channels(local_live_channels)
66 |
67 | self._stop.clear()
68 | self._logger.info('Twitch Checker is stopped')
69 |
70 | def _set_live_channels(self, ch_list):
71 | self._status_lock.acquire(True)
72 | self._live_channels = ch_list
73 | self._status_lock.release()
74 |
75 | def refresh(self):
76 | self._logger.debug('Trigger refresh')
77 | self._polling.set()
78 |
79 | def get_live_channels(self):
80 | self._status_lock.acquire(True)
81 | ch_stat = copy.copy(self._live_channels)
82 | self._status_lock.release()
83 | return ch_stat
84 |
85 | def async_stop(self):
86 | self._logger.debug('stop is called')
87 | self._polling.set()
88 | self._stop.set()
89 |
90 | def stop(self):
91 | self.async_stop()
92 | self._logger.debug('waiting for thread end')
93 | self.join()
94 |
95 | def is_stopped(self):
96 | return self._stop.is_set()
97 |
98 |
99 | class Service(ServiceBase):
100 | CMD = 'twitch'
101 | SUB_FILE = 'twitch_sublist.p'
102 | CHECK_PERIOD = 300
103 |
104 | def __init__(self, name_cache_size=512):
105 | ServiceBase.__init__(self)
106 | self._sublist_lock = Lock()
107 | self._twitch = TwitchEngine()
108 | self._channel_name_cache = LRUCache(name_cache_size)
109 |
110 | def _setup_argument(self, cmd_group):
111 | cmd_group.add_argument('-subscribe', nargs='+', func=self._subscribe,
112 | help='Subscribe channels and receive notification when channel goes live.\n'
113 | 'ex: {} -subscribe kaydada'.format(self.CMD))
114 | cmd_group.add_argument('-unsubscribe', nargs='+', func=self._unsubscribe,
115 | help='Unsubscribe channels.\n'
116 | 'ex: {} -unsubscribe kaydada'.format(self.CMD))
117 | cmd_group.add_argument('-unsuball', action='store_true', func=self._unsub_all,
118 | help="Unsubscribe all channels in Linot. I won't send any notification to you anymore.")
119 | cmd_group.add_argument('-listchannel', action='store_true', func=self._list_channel,
120 | help="List channels you've subscribed.")
121 | cmd_group.add_argument('-import', nargs=1, func=self._import,
122 | help='Import the following list of a twitch user.\n'
123 | 'ex: {} -import kaydada'.format(self.CMD))
124 |
125 | # below, admin only
126 | cmd_group.add_argument('-refresh', action='store_true', func=self._refresh,
127 | help=argparse.SUPPRESS)
128 | cmd_group.add_argument('-listusers', nargs='*', func=self._list_users,
129 | help=argparse.SUPPRESS)
130 | cmd_group.add_direct_command(self._sub_by_url, 'twitch\.tv/(\w+)[\s\t,]*', re.IGNORECASE)
131 |
132 | def _start(self):
133 | # Load subscribe list
134 | try:
135 | logger.debug('Loading subscribe list from file')
136 | self._sublist = pickle.load(open(self.SUB_FILE, 'rb'))
137 | self._calculate_channel_sub_count()
138 | except IOError:
139 | logger.debug('Subscribe list file not found, create empty.')
140 | self._sublist = defaultdict(list)
141 | self._channel_sub_count = defaultdict(int)
142 | self._check_thread = Checker(
143 | self.CHECK_PERIOD, self._twitch, self.get_sublist)
144 | self._check_thread.start()
145 |
146 | def _stop(self):
147 | self._check_thread.stop()
148 |
149 | def get_sublist(self):
150 | self._sublist_lock.acquire(True)
151 | local_sublist = copy.copy(self._sublist)
152 | self._sublist_lock.release()
153 | return local_sublist
154 |
155 | def _sub_by_url(self, match_iter, cmd, sender):
156 | logger.debug('sub by url: ' + str(match_iter))
157 | logger.debug('sub by url, direct cmd: ' + cmd)
158 | self._subscribe(match_iter, sender)
159 |
160 | def _calculate_channel_sub_count(self):
161 | self._channel_sub_count = defaultdict(int)
162 | for subr in self._sublist:
163 | for ch in self._sublist[subr]:
164 | self._channel_sub_count[ch] += 1
165 |
166 | def _import(self, twitch_user, sender):
167 | # get the following list of twitch_user and subscribe them for sender
168 | user = twitch_user[0]
169 | followed_channels = self._twitch.get_followed_channels(user)
170 | if followed_channels is None:
171 | sender.send_message('Twitch user: {} not found'.format(user))
172 | else:
173 | if len(followed_channels) > 8:
174 | sender.send_message('Number of followed channels is more than 8. It may take a while to process.')
175 | self._subscribe(followed_channels, sender)
176 |
177 | def _unsub_all(self, value, sender):
178 | # unsubscribe all channels for sender
179 | # we can not send self._sublist directly, since unsub operates
180 | # self._sublist
181 | user_sub = copy.copy(self._sublist[sender])
182 | self._unsubscribe(user_sub, sender)
183 |
184 | def _subscribe(self, chs, sender):
185 | # Handles user request for subscribing channels
186 | # We actually let the LinotServant to follow these channels
187 | # so that we can check if they are online use streams/followed API
188 |
189 | # prompt a message to let user know i am still alive...
190 | sender.send_message('Processing ...')
191 | msg = io.BytesIO()
192 |
193 | not_found = []
194 | for ch in chs:
195 | check_name = ch.lower()
196 | # reduce api invocation
197 | if check_name in self._sublist[sender]: # pragma: no cover
198 | continue
199 | ch_disp_name, stat = self._twitch.follow_channel(ch)
200 | if stat is False:
201 | not_found.append(ch)
202 | else:
203 | self._sublist_lock.acquire(True)
204 | self._sublist[sender].append(check_name)
205 | self._sublist_lock.release()
206 | self._channel_sub_count[check_name] += 1
207 | self._channel_name_cache.put(ch_disp_name.lower(), ch_disp_name)
208 | pickle.dump(self._sublist, open(self.SUB_FILE, 'wb+'))
209 |
210 | if len(not_found) > 0:
211 | print('Channel not found: ' + ' '.join(not_found), file=msg)
212 | print('Done', file=msg)
213 | sender.send_message(msg.getvalue())
214 | return
215 |
216 | def _unsubscribe(self, chs, sender):
217 | # prompt a message to let user know i am still alive...
218 | sender.send_message('Processing ...')
219 | msg = io.BytesIO()
220 |
221 | # Handles user request for unsubscribing channels
222 | not_found = []
223 | for ch in chs:
224 | check_name = ch.lower()
225 | self._sublist_lock.acquire(True)
226 | try:
227 | self._sublist[sender].remove(check_name)
228 | except ValueError:
229 | not_found.append(ch)
230 | self._sublist_lock.release()
231 | continue
232 | self._sublist_lock.release()
233 | self._channel_sub_count[check_name] -= 1
234 | if self._channel_sub_count[check_name] <= 0:
235 | # maybe we can try to not unfollow, so that we don't keep
236 | # generating follow message to the caster
237 | # self._twitch.unfollow_channel(ch)
238 | self._channel_sub_count.pop(check_name, None)
239 |
240 | if len(self._sublist[sender]) == 0:
241 | self._sublist_lock.acquire(True)
242 | self._sublist.pop(sender)
243 | self._sublist_lock.release()
244 |
245 | pickle.dump(self._sublist, open(self.SUB_FILE, 'wb+'))
246 | if len(not_found) > 0:
247 | print('Channel not found: ' + ' '.join(not_found), file=msg)
248 | print('Done', file=msg)
249 | sender.send_message(msg.getvalue())
250 | return
251 |
252 | def _list_channel(self, value, sender):
253 | msg = io.BytesIO()
254 | print('Your subscribed channels are:', file=msg)
255 | live_channels = self._check_thread.get_live_channels()
256 | for ch in self._sublist[sender]:
257 | if ch in [x.lower() for x in live_channels]:
258 | stat = '[LIVE]'
259 | else:
260 | stat = '[OFF]'
261 | display_name = self._channel_name_cache.get(ch)
262 | if display_name is None:
263 | display_name = self._twitch.get_channel_info(ch)['display_name']
264 | self._channel_name_cache.put(ch, display_name)
265 | print('{}\t{}'.format(stat, display_name), file=msg)
266 | sender.send_message(msg.getvalue())
267 |
268 | def _refresh(self, value, sender):
269 | #
270 | if sender.code == config['interface'][sender.interface_name]['admin_id']:
271 | self._check_thread.refresh()
272 | sender.send_message('Done')
273 |
274 | def _list_users(self, check_users, sender):
275 | # List all user who has subscription
276 | #
277 | if sender.code != config['interface'][sender.interface_name]['admin_id']:
278 | return
279 |
280 | user_list = self._sublist.keys()
281 | msg = io.StringIO()
282 | if len(check_users) == 0:
283 | # if no check_user list is inputed, list all user with sub count
284 | for user_index, user in enumerate(user_list):
285 | print(u'#{}) {}'.format(user_index, unicode(user)), file=msg)
286 | print(u'Subscribed count: {}'.format(len(self._sublist[user])), file=msg)
287 | print(u'----------------------------', file=msg)
288 | else:
289 | # list users sub channel list
290 | not_found = []
291 | for user_index in check_users:
292 | try:
293 | index = int(user_index)
294 | user = user_list[index]
295 | except (ValueError, IndexError):
296 | not_found.append(user_index)
297 | continue
298 |
299 | if user not in self._sublist:
300 | not_found.append(user_index)
301 | continue
302 |
303 | print(u'#{}) {}'.format(user_index, unicode(user)), file=msg)
304 | print(u'- Subscribed Channels: ', file=msg)
305 | for ch in self._sublist[user]:
306 | print(unicode(ch), end=u', ', file=msg)
307 | print(u'', file=msg)
308 | print(u'- Total Count: {}'.format(len(self._sublist[user])), file=msg)
309 | print(u'----------------------------', file=msg)
310 |
311 | if len(not_found) > 0:
312 | print(u'Not found: ', end=u'', file=msg)
313 | for na in not_found:
314 | print(unicode(na), end=u', ', file=msg)
315 | print(u'', file=msg)
316 |
317 | print(u'Done', file=msg) # make sure we are sending something to user
318 | sender.send_message(msg.getvalue())
319 | return
320 |
--------------------------------------------------------------------------------
/tests/services/test_twitch_notifier.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import inspect
3 | import pickle
4 | import threading
5 | import os
6 | import requests
7 | from collections import defaultdict
8 |
9 | from nose.tools import ok_
10 |
11 | from linot import interfaces
12 | from linot.command_submitter import CommandSubmitter
13 | from linot.services.twitch_notifier.service import Service, Checker
14 | from linot.services.twitch_notifier.twitch_engine import TwitchEngine
15 | from linot.arg_parser import LinotParser
16 | from linot import config
17 |
18 |
19 | class MockTwitchEngine:
20 | def __init__(self):
21 | self.exists_ch_list = []
22 | self.live_ch_list = {}
23 | self.followed_ch_list = {}
24 |
25 | def get_followed_channels(self, user):
26 | if user not in self.followed_ch_list:
27 | return None
28 | else:
29 | return self.followed_ch_list[user]
30 |
31 | def get_live_channels(self):
32 | return self.live_ch_list
33 |
34 | def follow_channel(self, ch):
35 | for exch in self.exists_ch_list:
36 | if exch.lower() == ch.lower():
37 | return exch, True
38 | return '', False
39 |
40 | def unfollow_channel(self, ch):
41 | pass
42 |
43 | def get_channel_info(self, ch):
44 | for exch in self.exists_ch_list:
45 | if exch.lower() == ch.lower():
46 | return {'display_name': exch}
47 | return None
48 |
49 | def set_exists_channel_list(self, ch_list):
50 | # swapcase to simulate display name does not necessary have the same
51 | # case as input
52 | self.exists_ch_list = [x.swapcase() for x in ch_list]
53 |
54 | def set_live_channel_list(self, ch_list):
55 | # swapcase to simulate display name does not necessary have the same
56 | # case as input
57 | for ch in ch_list:
58 | self.live_ch_list[ch.swapcase()] = ch_list[ch]
59 |
60 |
61 | class TestTwitchEngine:
62 | TWITCH_REST = 'https://api.twitch.tv/kraken'
63 |
64 | def setUp(self):
65 | self.twitch = TwitchEngine()
66 |
67 | def test_get_followed_channels(self):
68 | # twitch user should be following more than 25 users before this test
69 | followed_channels = self.twitch.get_followed_channels(self.twitch.USER)
70 | ok_(len(followed_channels) > 25,
71 | 'twitch user should be following more than'
72 | '25 users before running this test') # to make sure we have tested multiget
73 | ok_(len(set(followed_channels)) == len(followed_channels))
74 | for ch in followed_channels:
75 | expect_url = 'http://www.twitch.tv/'+ch.lower()
76 | ok_(followed_channels[ch]['url'] == expect_url,
77 | '{} <-> {}'.format(followed_channels[ch]['url'], expect_url))
78 |
79 | # test user not found returns None
80 | followed_channels = self.twitch.get_followed_channels(self.twitch.USER[:-2])
81 | ok_(followed_channels is None)
82 |
83 | def test_get_live_channels(self):
84 | # This is a tricky one, not sure how to properly test it..
85 | test_channel_count = 10
86 | live_channels = self.twitch.get_live_channels()
87 | error_count = 0
88 | test_count = 0
89 | for ch in live_channels:
90 | ret_json = requests.get(self.TWITCH_REST+'/streams/'+ch).json()
91 | try:
92 | ok_(ret_json['stream']['channel']['display_name'] == ch)
93 | except KeyError:
94 | error_count += 1
95 | test_count += 1
96 | if test_count >= test_channel_count:
97 | break
98 | # there is time difference between get live and check live
99 | # it is possible that channel went offline between these 2 api calls
100 | # so we just expect 80% of tested channels are really live on the
101 | # 2nd api call
102 | ok_((float(error_count) / test_count) < 0.20, 'test:{}, error:{}'.format(test_count, error_count))
103 |
104 | def test_follow_unfollow_channel(self):
105 | self.twitch.unfollow_channel('kaydada')
106 | followed_channels = self.twitch.get_followed_channels(self.twitch.USER)
107 | ok_('KayDaDa' not in followed_channels)
108 | self.twitch.follow_channel('kaydada')
109 | followed_channels = self.twitch.get_followed_channels(self.twitch.USER)
110 | ok_('KayDaDa' in followed_channels)
111 | ret = self.twitch.unfollow_channel('kaydada')
112 | ok_(ret is True)
113 | followed_channels = self.twitch.get_followed_channels(self.twitch.USER)
114 | ok_('KayDaDa' not in followed_channels)
115 | name, ret = self.twitch.follow_channel('kaydada2')
116 | ok_(ret is False)
117 | ret = self.twitch.unfollow_channel('kaydada2')
118 | ok_(ret is False)
119 | name, ret = self.twitch.follow_channel('kaydada')
120 | ok_(ret is True)
121 |
122 | def test_get_channel_info(self):
123 | info = self.twitch.get_channel_info('kaydada')
124 | ok_(info['display_name'] == 'KayDaDa')
125 |
126 | info = self.twitch.get_channel_info('kaydada_no_this_guy')
127 | ok_(info is None, info)
128 |
129 |
130 | class TestChecker:
131 | def setUp(self):
132 | self.twitch = MockTwitchEngine()
133 | self.checker = Checker(300, self.twitch, self.get_sublist)
134 | self.sublist = {}
135 | interfaces.get('test').reset()
136 |
137 | def get_sublist(self):
138 | return self.sublist
139 |
140 | def test_stop_on_processing(self):
141 | self.checker.start()
142 | threading.Event().wait(1)
143 | self.checker.refresh()
144 |
145 | def locker():
146 | locker.event.wait()
147 | return {}
148 | locker.event = threading.Event()
149 | self.checker._twitch.get_live_channels = locker
150 | threading.Event().wait(2) # expect checker wait on getLiveChannels
151 | locker.event.set()
152 |
153 | self.checker.async_stop()
154 | ok_(self.checker.is_stopped())
155 | self.checker.join(5)
156 | ok_(not self.checker.is_alive())
157 |
158 | def test_skip_already_lived_on_boot(self):
159 | self.twitch.live_ch_list = {
160 | 'testch1': {
161 | 'status': 'test_status',
162 | 'game': 'test_game',
163 | 'url': 'test_url'
164 | }
165 | }
166 | fake_sender = CommandSubmitter('test', 'fake_sender')
167 | self.sublist = {fake_sender: ['testch1']}
168 |
169 | self.checker.start()
170 | threading.Event().wait(1)
171 | self.checker.stop()
172 | ok_(len(interfaces.get('test').msg_queue[fake_sender]) == 0)
173 |
174 | def test_live_notification(self):
175 | fake_sender = CommandSubmitter('test', 'fake_sender')
176 | fake_sender2 = CommandSubmitter('test', 'fake_sender2')
177 | self.sublist = {
178 | fake_sender: ['testch1'],
179 | fake_sender2: ['testch2'],
180 | }
181 |
182 | self.checker.start()
183 | threading.Event().wait(1)
184 | self.checker.refresh()
185 | threading.Event().wait(1)
186 | ok_(len(interfaces.get('test').msg_queue[fake_sender]) == 0)
187 | ok_(len(interfaces.get('test').msg_queue[fake_sender2]) == 0)
188 |
189 | self.twitch.live_ch_list = {
190 | 'TESTCH1': {
191 | 'status': u'test_status',
192 | 'game': u'test_game',
193 | 'url': u'test_url'
194 | }
195 | }
196 | self.checker.refresh()
197 | threading.Event().wait(1)
198 | self.checker.stop()
199 | ok_('TESTCH1' in ' '.join(interfaces.get('test').msg_queue[fake_sender]),
200 | interfaces.get('test').msg_queue[fake_sender])
201 | ok_('TESTCH1' not in ' '.join(interfaces.get('test').msg_queue[fake_sender2]),
202 | interfaces.get('test').msg_queue[fake_sender2])
203 |
204 | def test_channel_offline(self):
205 | self.twitch.live_ch_list = {
206 | 'testch1': {
207 | 'status': u'test_status',
208 | 'game': u'test_game',
209 | 'url': u'test_url'
210 | }
211 | }
212 | fake_sender = CommandSubmitter('test', 'fake_sender')
213 | self.sublist = {fake_sender: ['testch1']}
214 | self.checker.start()
215 | threading.Event().wait(1)
216 | self.twitch.live_ch_list = {}
217 | self.checker.refresh()
218 | threading.Event().wait(1)
219 | # no message should be sent
220 | ok_(len(interfaces.get('test').msg_queue[fake_sender]) == 0)
221 |
222 | # simulate channel goes live
223 | self.twitch.live_ch_list = {
224 | 'testch1': {
225 | 'status': u'test_status',
226 | 'game': u'test_game',
227 | 'url': u'test_url'
228 | }
229 | }
230 | self.checker.refresh()
231 | threading.Event().wait(1)
232 | self.checker.stop()
233 | ok_('testch1' in ' '.join(interfaces.get('test').msg_queue[fake_sender]),
234 | interfaces.get('test').msg_queue[fake_sender])
235 |
236 |
237 | class TestService:
238 | def setUp(self):
239 | self.service = Service()
240 | self.service._twitch = MockTwitchEngine()
241 | self.parser = LinotParser(usage=argparse.SUPPRESS, add_help=False)
242 | interfaces.get('test').reset()
243 |
244 | def tearDown(self):
245 | try:
246 | os.remove(self.service.SUB_FILE)
247 | except OSError:
248 | pass
249 |
250 | def test_init(self):
251 | # some basic tests
252 | ok_(self.service.is_start() is False)
253 | ok_(self.service.CMD == 'twitch')
254 |
255 | def test_setup_argument(self):
256 | def set_matching_0(val, sender):
257 | set_matching_0.ret = (len(val) == 0)
258 |
259 | def set_matching_1(val, sender):
260 | set_matching_1.ret = (set(val) == set(['test1']))
261 |
262 | def set_matching_2(val, sender):
263 | set_matching_2.ret = (set(val) == set(['test1', 'test2']))
264 |
265 | def ret_val(val, sender):
266 | ret_val.ret = val
267 |
268 | def direct_cmd_set_match(match, cmd, sender):
269 | direct_cmd_set_match.ret = (set(match) == set(['kaydada']))
270 |
271 | # test argument and function relation
272 | test_list = [
273 | # -subscribe +
274 | ['-subscribe test1', '_subscribe', set_matching_1],
275 | ['-subscribe test1 test2', '_subscribe', set_matching_2],
276 | # -unsubscribe +
277 | ['-unsubscribe test1', '_unsubscribe', set_matching_1],
278 | ['-unsubscribe test1 test2', '_unsubscribe', set_matching_2],
279 | # -unsuball
280 | ['-unsuball', '_unsub_all', ret_val],
281 | ['-unsuball abc', '_unsub_all', ret_val],
282 | # -import
283 | ['-import test1', '_import', set_matching_1],
284 | # -listchannel
285 | ['-listchannel', '_list_channel', ret_val],
286 | ['-listchannel abc', '_list_channel', ret_val],
287 | # -refresh
288 | ['-refresh', '_refresh', ret_val],
289 | ['-refresh abc', '_refresh', ret_val],
290 | # -listusers *
291 | ['-listusers', '_list_users', set_matching_0],
292 | ['-listusers test1', '_list_users', set_matching_1],
293 | ['-listusers test1 test2', '_list_users', set_matching_2],
294 | ]
295 |
296 | for test in test_list:
297 | self.setUp()
298 | method_bak = None
299 | for name, method in inspect.getmembers(self.service, predicate=inspect.ismethod):
300 | if name == test[1]:
301 | method_bak = method
302 | setattr(self.service, name, test[2])
303 | ok_(method_bak is not None, test[0] + ' case method not found')
304 | self.service.setup(self.parser) # setup calls _setup_arguments
305 | cmd = self.service.CMD + ' ' + test[0]
306 | args, unknowns = self.parser.parse_known_args(cmd.split())
307 | test[2].ret = False
308 | args.proc(args, None)
309 | ok_(test[2].ret, 'failed: '+test[0])
310 |
311 | # test direct command
312 | test_list = [
313 | # direct command: sub by url
314 | ['www.twitch.tv/kaydada', '_sub_by_url', direct_cmd_set_match],
315 | ]
316 |
317 | for test in test_list:
318 | self.setUp()
319 | method_bak = None
320 | for name, method in inspect.getmembers(self.service, predicate=inspect.ismethod):
321 | if name == test[1]:
322 | method_bak = method
323 | setattr(self.service, name, test[2])
324 | ok_(method_bak is not None, test[0] + ' case method not found')
325 | self.service.setup(self.parser) # setup calls _setup_arguments
326 | cmd = test[0]
327 | self.parser.process_direct_commands(cmd, None)
328 | ok_(test[2].ret, 'failed: '+test[0])
329 |
330 | def test_start_subfile_exists(self):
331 | fake_sublist = {
332 | 'testid1': ['testch1', 'testch2', 'testch3'],
333 | 'testid2': ['testch2'],
334 | 'testid3': ['testch1', 'testch3'],
335 | 'testid4': ['testch2'],
336 | }
337 | pickle.dump(fake_sublist, open(self.service.SUB_FILE, 'wb+'))
338 | self.service.start()
339 | threading.Event().wait(.1)
340 | self.service.stop()
341 | ok_(self.service._channel_sub_count['testch1'] == 2, 'count =' + str(self.service._channel_sub_count['testch1']))
342 | ok_(self.service._channel_sub_count['testch2'] == 3, 'count =' + str(self.service._channel_sub_count['testch2']))
343 | ok_(self.service._channel_sub_count['testch3'] == 2, 'count =' + str(self.service._channel_sub_count['testch3']))
344 | ok_(self.service._channel_sub_count['testch4'] == 0, 'count =' + str(self.service._channel_sub_count['testch4']))
345 |
346 | def test_start_subfile_not_found(self):
347 | self.service.start()
348 | threading.Event().wait(.1)
349 | self.service.stop()
350 | ok_(set(self.service._sublist['testid1']) == set([]))
351 | ok_(set(self.service._sublist['testid2']) == set([]))
352 | ok_(self.service._channel_sub_count['testch1'] == 0, 'count =' + str(self.service._channel_sub_count['testch1']))
353 | ok_(self.service._channel_sub_count['testch2'] == 0, 'count =' + str(self.service._channel_sub_count['testch2']))
354 |
355 | def test_get_sublist(self):
356 | fake_sublist = {
357 | 'testid1': ['testch1', 'testch2', 'testch3'],
358 | 'testid2': ['testch2'],
359 | 'testid3': ['testch1', 'testch3'],
360 | }
361 | pickle.dump(fake_sublist, open(self.service.SUB_FILE, 'wb+'))
362 | self.service.start()
363 | threading.Event().wait(.1)
364 | self.service.stop()
365 | sublist = self.service.get_sublist()
366 | ok_(len(fake_sublist.viewkeys() ^ sublist.viewkeys()) == 0)
367 | for key in fake_sublist:
368 | ok_(set(fake_sublist[key]) == set(sublist[key]))
369 |
370 | def test_calculate_channel_count(self):
371 | self.service._sublist = {
372 | 'testid1': ['testch1', 'testch2', 'testch3'],
373 | 'testid2': ['testch2'],
374 | 'testid3': ['testch1', 'testch3', 'testch2'],
375 | }
376 | self.service._calculate_channel_sub_count()
377 | ok_(self.service._channel_sub_count['testch1'] == 2)
378 | ok_(self.service._channel_sub_count['testch2'] == 3)
379 | ok_(self.service._channel_sub_count['testch3'] == 2)
380 | ok_(self.service._channel_sub_count['testch4'] == 0)
381 | self.service._sublist = defaultdict(list)
382 | self.service._calculate_channel_sub_count()
383 | ok_(self.service._channel_sub_count['testch1'] == 0)
384 | ok_(self.service._channel_sub_count['testch2'] == 0)
385 | ok_(self.service._channel_sub_count['testch3'] == 0)
386 | ok_(self.service._channel_sub_count['testch4'] == 0)
387 |
388 | def test_subscribe_one(self):
389 | try:
390 | os.remove(self.service.SUB_FILE)
391 | except OSError:
392 | pass
393 |
394 | self.service.start()
395 | threading.Event().wait(.1)
396 | self.service.stop()
397 |
398 | self.service._twitch.set_exists_channel_list(['testch1', 'testch2'])
399 | fake_sender = CommandSubmitter('test', 'fake_sender')
400 | self.service._subscribe(['testch1'], fake_sender)
401 |
402 | ok_('testch1' in [x.lower() for x in self.service._sublist[fake_sender]],
403 | 'sublist = '+str(self.service._sublist[fake_sender]))
404 | ok_('not found' not in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
405 | ok_(self.service._channel_sub_count['testch1'] == 1)
406 |
407 | fake_sender2 = CommandSubmitter('test', 'fake_sender2')
408 | self.service._subscribe(['testch1'], fake_sender2)
409 |
410 | ok_(self.service._channel_sub_count['testch1'] == 2)
411 |
412 | def test_subscribe_one_exists(self):
413 | try:
414 | os.remove(self.service.SUB_FILE)
415 | except OSError:
416 | pass
417 | self.service.start()
418 | threading.Event().wait(.1)
419 | self.service.stop()
420 |
421 | self.service._twitch.set_exists_channel_list(['testch1', 'testch2'])
422 | fake_sender = CommandSubmitter('test', 'fake_sender')
423 | self.service._sublist[fake_sender] = ['testch1']
424 | self.service._subscribe(['testch1'], fake_sender)
425 |
426 | ok_(self.service._sublist[fake_sender].count('testch1') == 1,
427 | 'sublist = '+str(self.service._sublist[fake_sender]))
428 | ok_('not found' not in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
429 |
430 | def test_subscribe_one_not_found(self):
431 | self.service.start()
432 | threading.Event().wait(.1)
433 | self.service.stop()
434 |
435 | self.service._twitch.set_exists_channel_list(['testch2', 'testch3'])
436 | fake_sender = CommandSubmitter('test', 'fake_sender')
437 | self.service._subscribe(['testch1'], fake_sender)
438 |
439 | ok_('testch1' not in self.service._sublist[fake_sender],
440 | 'sublist = '+str(self.service._sublist[fake_sender]))
441 | ok_('not found' in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
442 |
443 | def test_subscribe_multi(self):
444 | try:
445 | os.remove(self.service.SUB_FILE)
446 | except OSError:
447 | pass
448 | self.service.start()
449 | threading.Event().wait(.1)
450 | self.service.stop()
451 |
452 | self.service._twitch.set_exists_channel_list(['testch1', 'testch2'])
453 | fake_sender = CommandSubmitter('test', 'fake_sender')
454 | self.service._subscribe(['testch1', 'testch2'], fake_sender)
455 | ok_('testch1' in self.service._sublist[fake_sender],
456 | 'sublist = '+str(self.service._sublist[fake_sender]))
457 | ok_('testch2' in self.service._sublist[fake_sender],
458 | 'sublist = '+str(self.service._sublist[fake_sender]))
459 | ok_('not found' not in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
460 |
461 | def test_unsubscribe_one(self):
462 | self.test_subscribe_one()
463 |
464 | fake_sender = CommandSubmitter('test', 'fake_sender')
465 | fake_sender2 = CommandSubmitter('test', 'fake_sender2')
466 | self.service._subscribe(['testch2'], fake_sender)
467 | self.service._unsubscribe(['testch1'], fake_sender)
468 |
469 | ok_('testch1' not in self.service._sublist[fake_sender],
470 | 'sublist = '+str(self.service._sublist[fake_sender]))
471 | ok_('not found' not in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
472 | ok_(self.service._channel_sub_count['testch1'] == 1)
473 |
474 | self.service._unsubscribe(['testch1'], fake_sender2)
475 |
476 | ok_('testch1' not in self.service._sublist[fake_sender2],
477 | 'sublist = '+str(self.service._sublist[fake_sender2]))
478 | ok_(self.service._channel_sub_count['testch1'] == 0)
479 |
480 | def test_unsubscribe_one_not_found(self):
481 | self.test_subscribe_one()
482 |
483 | fake_sender = CommandSubmitter('test', 'fake_sender')
484 | self.service._subscribe(['testch2'], fake_sender)
485 | self.service._unsubscribe(['testch3'], fake_sender)
486 |
487 | ok_('testch1' in self.service._sublist[fake_sender],
488 | 'sublist = '+str(self.service._sublist[fake_sender]))
489 | ok_('testch2' in self.service._sublist[fake_sender],
490 | 'sublist = '+str(self.service._sublist[fake_sender]))
491 | ok_('not found' in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
492 | ok_(self.service._channel_sub_count['testch1'] == 2)
493 |
494 | def test_unsubscribe_multi_not_found(self):
495 | self.test_subscribe_one()
496 |
497 | fake_sender = CommandSubmitter('test', 'fake_sender')
498 | self.service._subscribe(['testch2'], fake_sender)
499 | self.service._unsubscribe(['testch3', 'testch1', 'testch2'], fake_sender)
500 |
501 | ok_('testch1' not in self.service._sublist[fake_sender],
502 | 'sublist = '+str(self.service._sublist[fake_sender]))
503 | ok_('testch2' not in self.service._sublist[fake_sender],
504 | 'sublist = '+str(self.service._sublist[fake_sender]))
505 | ok_('not found' in ' '.join(interfaces.get('test').msg_queue[fake_sender]))
506 | ok_(self.service._channel_sub_count['testch1'] == 1)
507 |
508 | def test_list_channel(self):
509 | self.service._twitch.set_live_channel_list({'testch2': {'display_name': 'TESTCH2'}})
510 | self.test_subscribe_multi()
511 |
512 | fake_sender = CommandSubmitter('test', 'fake_sender')
513 | self.service._list_channel(True, fake_sender)
514 | ok_(' '.join(interfaces.get('test').msg_queue[fake_sender]).count('LIVE') == 1)
515 |
516 | check_msg = ' '.join(interfaces.get('test').msg_queue[fake_sender])
517 | ok_('testch2' in check_msg.lower())
518 | ok_('testch1' in check_msg.lower())
519 |
520 | def test_list_channel_invalidate_cache(self):
521 | self.service._twitch.set_live_channel_list({'testch2': {'display_name': 'TESTCH2'}})
522 | self.test_subscribe_multi()
523 | self.service._channel_name_cache.clear()
524 |
525 | fake_sender = CommandSubmitter('test', 'fake_sender')
526 | self.service._list_channel(True, fake_sender)
527 | ok_(' '.join(interfaces.get('test').msg_queue[fake_sender]).count('LIVE') == 1)
528 |
529 | check_msg = ' '.join(interfaces.get('test').msg_queue[fake_sender])
530 | ok_('testch2' in check_msg.lower())
531 | ok_('testch1' in check_msg.lower())
532 |
533 | def test_refresh(self):
534 | # check admin only
535 | self.service.start()
536 | threading.Event().wait(.1)
537 | self.service.stop()
538 |
539 | def self_ret():
540 | self_ret.val = True
541 | self_ret.val = False
542 |
543 | self.service._check_thread.refresh = self_ret
544 |
545 | config['interface']['test'] = {'admin_id': 'test_admin'}
546 | fake_sender = CommandSubmitter('test', 'fake_sender')
547 | self.service._refresh(True, fake_sender)
548 |
549 | ok_(self_ret.val is not True)
550 |
551 | self.service._refresh(True, CommandSubmitter('test', 'test_admin'))
552 | ok_(self_ret.val is True)
553 |
554 | def test_list_users_no_user(self):
555 | # issue #10, list user hit exception while there is no user
556 | config['interface']['test'] = {'admin_id': 'test_admin'}
557 | try:
558 | os.remove(self.service.SUB_FILE)
559 | except OSError:
560 | pass
561 |
562 | self.service.start()
563 | threading.Event().wait(.1)
564 | self.service.stop()
565 |
566 | fake_sender = CommandSubmitter('test', 'test_admin')
567 | self.service._list_users([], fake_sender)
568 |
569 | # check there is a response to user
570 | ok_(len(''.join(interfaces.get('test').msg_queue[fake_sender])) > 0)
571 |
572 | def test_list_users(self):
573 | self.test_subscribe_one()
574 | self.service._subscribe(['testch2'], CommandSubmitter('test', 'fake_sender2'))
575 | config['interface']['test'] = {'admin_id': 'test_admin'}
576 |
577 | # check admin only
578 | fake_sender = CommandSubmitter('test', 'fake_sender')
579 | self.service._list_users([], fake_sender)
580 | ok_('Channels' not in ''.join(interfaces.get('test').msg_queue[fake_sender]))
581 |
582 | fake_sender = CommandSubmitter('test', 'test_admin')
583 | self.service._list_users([], fake_sender)
584 |
585 | # check msg response
586 | ok_('fake_sender' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
587 | ok_('fake_sender2' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
588 | ok_('testch1' not in ''.join(interfaces.get('test').msg_queue[fake_sender]))
589 | ok_('testch2' not in ''.join(interfaces.get('test').msg_queue[fake_sender]))
590 |
591 | # test list single user, channels
592 | interfaces.get('test').reset()
593 | for index in range(2):
594 | self.service._list_users([index], fake_sender)
595 | msg = ' '.join(interfaces.get('test').msg_queue[fake_sender])
596 |
597 | if 'fake_sender' in msg:
598 | ok_('testch1' in msg)
599 | elif 'fake_sender2' in msg:
600 | ok_('testch2' in msg)
601 | ok_('testch1' in msg)
602 |
603 | # test list multiple user channels
604 | interfaces.get('test').reset()
605 | self.service._list_users(range(2), fake_sender)
606 | ok_('fake_sender' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
607 | ok_('fake_sender2' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
608 | ok_('testch1' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
609 | ok_('testch2' in ''.join(interfaces.get('test').msg_queue[fake_sender]))
610 |
611 | def test_sub_by_url(self):
612 | sender = CommandSubmitter('test', 'sender')
613 | self.service._twitch.exists_ch_list = ['KayDaDa', 'LinotServant']
614 | self.service.setup(self.parser)
615 | self.service.start()
616 | threading.Event().wait(1)
617 | self.service.stop()
618 | self.service._sub_by_url(['KayDaDa', 'LinotServant'], 'dummy', sender)
619 | ok_('KayDaDa'.lower() in self.service._sublist[sender],
620 | 'sublist = '+str(self.service._sublist[sender]))
621 | ok_('LinotServant'.lower() in self.service._sublist[sender],
622 | 'sublist = '+str(self.service._sublist[sender]))
623 |
624 | self.service._unsubscribe(['KayDaDa', 'LinotServant'], sender)
625 | ok_('KayDaDa'.lower() not in self.service._sublist[sender],
626 | 'sublist = '+str(self.service._sublist[sender]))
627 | ok_('LinotServant'.lower() not in self.service._sublist[sender],
628 | 'sublist = '+str(self.service._sublist[sender]))
629 |
630 | # Integration test
631 | self.parser.process_direct_commands('www.twitch.tv/KayDaDa twitch.tv/LinotServant', sender)
632 | ok_('KayDaDa'.lower() in self.service._sublist[sender],
633 | 'sublist = '+str(self.service._sublist[sender]))
634 | ok_('LinotServant'.lower() in self.service._sublist[sender],
635 | 'sublist = '+str(self.service._sublist[sender]))
636 |
637 | def test_unsub_all(self):
638 | sender = CommandSubmitter('test', 'sender')
639 | test_channels = ['testch1', 'testch2', 'testch3']
640 | self.service._twitch.set_exists_channel_list(test_channels)
641 |
642 | self.service.setup(self.parser)
643 | self.service.start()
644 | threading.Event().wait(1)
645 | self.service.stop()
646 |
647 | self.service._subscribe(test_channels, sender)
648 | self.service._list_channel(True, sender)
649 | # make sure subscribe success
650 | for ch in test_channels:
651 | ok_(ch.lower() in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
652 |
653 | interfaces.get('test').reset()
654 |
655 | self.service._unsub_all(True, sender)
656 | self.service._list_channel(True, sender)
657 | for ch in test_channels:
658 | ok_(ch.lower() not in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
659 |
660 | def test_import(self):
661 | sender = CommandSubmitter('test', 'tester')
662 | short_test_channels = ['testch'+str(x) for x in range(3)]
663 | long_test_channels = ['testch'+str(x) for x in range(10)]
664 |
665 | self.service.setup(self.parser)
666 | self.service.start()
667 | threading.Event().wait(1)
668 | self.service.stop()
669 |
670 | # test small amount of followed channel
671 | self.service._twitch.followed_ch_list = {'some_twitch_user': short_test_channels}
672 | self.service._import(['some_twitch_user'], sender)
673 | self.service._list_channel(True, sender)
674 | for ch in short_test_channels:
675 | ok_(ch.lower() in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
676 | interfaces.get('test').reset()
677 | self.service._unsub_all(True, sender)
678 |
679 | # test large amount of followed channel
680 | self.service._twitch.followed_ch_list = {'some_twitch_user': long_test_channels}
681 | self.service._import(['some_twitch_user'], sender)
682 | self.service._list_channel(True, sender)
683 | for ch in long_test_channels:
684 | ok_(ch.lower() in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
685 | ok_('a while' in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
686 | interfaces.get('test').reset()
687 | self.service._unsub_all(True, sender)
688 |
689 | # test not found
690 | self.service._twitch.followed_ch_list = {'some_twitch_user': long_test_channels}
691 | self.service._import(['some_twitch_user2'], sender)
692 | self.service._list_channel(True, sender)
693 | for ch in long_test_channels:
694 | ok_(ch.lower() not in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
695 | ok_('not found' in ' '.join(interfaces.get('test').msg_queue[sender]).lower())
696 | interfaces.get('test').reset()
697 | self.service._unsub_all(True, sender)
698 |
--------------------------------------------------------------------------------