├── 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 | [![Join the chat at https://gitter.im/KavenC/Linot](https://badges.gitter.im/Join%20Chat.svg)](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 | --------------------------------------------------------------------------------