def load(namespace, name): """ Attempts to load a plugin using a PluginManager. """ manager = PluginManager(namespace) with console.status(f"Loading {namespace}:{name}"): then = time.time() plugin = manager.load(name) took = time.time() - then rprint( f":tada: successfully loaded [bold][green]{namespace}[/green][/bold]:[bold][cyan]{name}[/cyan][/bold] ({type(plugin)}" ) rprint(f":stopwatch: loading took {took:.4f} s")
class Bot(Client): def __init__(self, core, configfile): self.core = core self.configfile = configfile self.config = ConfigParser() self.config.read(configfile) host = self.config.get('base', 'host') port = self.config.getint('base', 'port') try: ssl = self.config.getboolean('base', 'ssl') except: ssl = False Client.__init__(self, (host, port), ssl) self.hooks = HookManager(self) self.plugins = PluginManager(self) self.hooks.install_owner(self) self.nick = None self.channels = {} superuser = self.config.get('base', 'superuser') self.allow_rules = {'*': {'ANY': 1}, superuser: {'ANY': 1000}} self.deny_rules = {} self._name = '_bot' autoload = self.config.get('base', 'autoload').split() for name in autoload: self.plugins.load(name) self.connect() def set_timer(self, fn, timestamp, owner=None): hook = TimestampHook(timestamp) hook.bind(fn, owner) self.hooks.install(hook) return hook def set_interval(self, fn, seconds, owner=None): hook = TimestampHook(time() + seconds, {'repeat': seconds}) hook.bind(fn, owner) self.hooks.install(hook) return hook def set_timeout(self, fn, seconds, owner=None): hook = TimestampHook(time() + seconds) hook.bind(fn, owner) self.hooks.install(hook) return hook def do_tick(self, timestamp): self.hooks.call_timestamp(timestamp) def privmsg(self, target, text): wraplen = 510 wraplen -= 1 + len(self.nick) # ":<nick>" wraplen -= 1 + 10 # "!<user>" wraplen -= 1 + 63 # "@<host>" wraplen -= 9 # " PRIVMSG " wraplen -= len(target) # "<target>" wraplen -= 2 # " :" for line in wrap(text, wraplen): self.send('PRIVMSG %s :%s' % (target, line)) def notice(self, target, text): wraplen = 510 wraplen -= 1 + len(self.nick) # ":<nick>" wraplen -= 1 + 10 # "!<user>" wraplen -= 1 + 63 # "@<host>" wraplen -= 8 # " NOTICE " wraplen -= len(target) # "<target>" wraplen -= 2 # " :" for line in wrap(text, wraplen): self.send('NOTICE %s :%s' % (target, line)) def join(self, channels, keys=None): if isinstance(channels, str): channels = (channels,) if channels: channel_s = ','.join(channels) if keys: if isinstance(keys, str): keys = (keys,) key_s = ','.join(keys) self.send('JOIN %s %s' % (channel_s, key_s)) pairs = list(zip(channels, keys)) for item in pairs: self.channels[item[0]] = {'key': item[1], 'joined': False, 'nicks': set()} else: self.send('JOIN %s' % channel_s) for channel in channels: self.channels[channel] = {'joined': False, 'nicks': set()} def part(self, channels, message=None): if type(channels) == str: channels = (channels,) if channels: channels = ','.join(channels) if message: self.send('PART %s :%s' % (channels, message)) else: self.send('PART %s' % channels) @hook @priority(0) def disconnect_event(self): for _, props in list(self.channels.items()): props['joined'] = False props['nicks'].clear() @hook @priority(0) def shutdown_event(self, reason): self.send('QUIT :%s' % reason) for name in self.plugins.list(): self.plugins.unload(name, True) @hook def _001_command(self, msg): self.server = msg.source self.nick = msg.param[0] @hook def _353_command(self, msg): channel = msg.param[2] if channel in self.channels and self.channels[channel]['joined']: nicks = self.channels[channel]['nicks'] for nick in msg.param[-1].split(): if nick.startswith(('~', '&', '@', '%', '+')): nicks.add(nick[1:]) else: nicks.add(nick) @hook def join_command(self, msg): channel = msg.param[0] if msg.source == self.nick: if channel not in self.channels: self.channels[channel] = {} self.channels[channel]['joined'] = True elif channel in self.channels: self.channels[channel]['nicks'].add(msg.source) @hook def kick_command(self, msg): channel = msg.param[0] if msg.param[1] == self.nick: if channel in self.channels: self.channels[channel]['joined'] = False if 'nicks' in self.channels[channel]: self.channels[channel]['nicks'].clear() elif channel in self.channels: self.channels[channel]['nicks'].remove(msg.source) @hook def nick_command(self, msg): new_nick = msg.param[0] if msg.source == self.nick: self.nick = new_nick for _, props in list(self.channels.items()): if 'nicks' in props and msg.source in props['nicks']: props['nicks'].remove(msg.source) props['nicks'].add(new_nick) @hook @priority(0) def part_command(self, msg): channel = msg.param[0] if msg.source == self.nick: if channel in self.channels: self.channels[channel]['joined'] = False if 'nicks' in self.channels[channel]: self.channels[channel]['nicks'].clear() elif channel in self.channels: self.channels[channel]['nicks'].remove(msg.source) @hook def ping_command(self, msg): self.send('PONG :%s' % msg.param[-1]) @hook @priority(0) def quit_command(self, msg): for _, props in list(self.channels.items()): if 'nicks' in props and msg.source in props['nicks']: props['nicks'].remove(msg.source)
class Player(): def __init__(self, account_id, private_key, provider, dev=False): self.plugin = PluginManager() self.plugin.load(PLUGINS_PATH) self.plugin.set_default_solverclass('gcs_solver.py') self.dev = dev self.account_id = account_id self.web3 = Web3(provider) self.interest = '' self.trusted_users = [] self.web3.eth.defaultAccount = account_id # PoA であれば geth_poa_middleware を利用 try: self.web3.eth.getBlock("latest") except ExtraDataLengthError: self.web3.middleware_onion.inject(geth_poa_middleware, layer=0) if private_key: self.web3.middleware_onion.add( construct_sign_and_send_raw_middleware(private_key)) self.deploy_erc1820() self.__observer = None self.__state = None self.assets = None # Wallet の情報 self.wallet = Wallet(self.web3, self.account_id) # オペレータ(トークンの交換などを担当)のコントラクト self.operator_address = None self.load_config() self.operator_address = self._fix_config_address( self.config['operator']['address']) if self.config['operator']['solver_pluginfile']: self.plugin.set_solverclass( self.operator_address, self.config['operator']['solver_pluginfile']) self.contracts = Contracts(self.web3) self.deploy_metemcyberutil() self.fetch_trusted_users() self.event_listener = BasicEventListener('') self.event_listener.start() # inventory (トークン・カタログの管理)のインスタンス生成 catalog_address = self._fix_config_address( self.config['catalog']['address']) broker_address = self._fix_config_address( self.config['broker']['address']) self.inventory = Inventory(self.contracts, self.account_id, self.event_listener, catalog_address, broker_address) # Seeker (チャレンジの依頼者)のインスタンス self.seeker = Seeker(self.contracts) # Solver (チャレンジの受領者)としてのインスタンス if self.operator_address: solverclass = self.plugin.get_solverclass(self.operator_address) self.solver = solverclass(self.contracts, self.account_id, self.operator_address) else: self.solver = None # MISP設定のinsert self.load_misp_config(MISP_INI_FILEPATH) def deploy_erc1820(self): # ERC777を利用するにはERC1820が必要 # https://github.com/ConsenSys/ERC1400/blob/master/migrations/2_erc1820_registry.js deployer_address = '0xa990077c3205cbDf861e17Fa532eeB069cE9fF96' contract_address = Web3.toChecksumAddress( '0x1820a4b7618bde71dce8cdc73aab6c95905fad24') code = self.web3.eth.getCode(contract_address) LOGGER.debug('erc1820_address has %s', code) if not code: #指定のアドレスへ送金 tx_hash = self.web3.eth.sendTransaction({ 'from': self.account_id, 'to': deployer_address, 'value': self.web3.toWei('0.1', 'ether') }) tx_receipt = self.web3.eth.waitForTransactionReceipt(tx_hash) GASLOG.info('erc1820.sendTransaction: gasUsed=%d', tx_receipt['gasUsed']) LOGGER.debug(tx_receipt) #ERC1820のデプロイ with open(ERC1820_RAW_TX_FILEPATH, 'r') as fin: raw_tx = fin.read().strip() tx_hash = self.web3.eth.sendRawTransaction(raw_tx) tx_receipt = self.web3.eth.waitForTransactionReceipt(tx_hash) GASLOG.info('erc1820.sendRawTransaction: gasUsed=%d', tx_receipt['gasUsed']) LOGGER.debug(tx_receipt) def deploy_metemcyberutil(self): metemcyber_util = self.contracts.accept(MetemcyberUtil()) if not self.config['metemcyber_util']['address']: self.config['metemcyber_util']['address'] = \ metemcyber_util.new().contract_address placeholder = metemcyber_util.register_library( self.config['metemcyber_util']['address'], self.config['metemcyber_util']['placeholder']) if placeholder != self.config['metemcyber_util']['placeholder']: self.config['metemcyber_util']['placeholder'] = placeholder self.save_config() def _fix_config_address(self, target): if self.web3.isChecksumAddress(target): return target if self.web3.isAddress(target): return self.web3.toChecksumAddress(target) return None def load_config(self): # コントラクトのアドレスを設定ファイルから読み込む fname = CONFIG_INI_FILEPATH config = configparser.ConfigParser() config.add_section('catalog') config.set('catalog', 'address', '') config.add_section('broker') config.set('broker', 'address', '') config.add_section('operator') config.set('operator', 'address', '') config.set('operator', 'owner', '') config.set('operator', 'solver_pluginfile', '') config.add_section('metemcyber_util') config.set('metemcyber_util', 'address', '') config.set('metemcyber_util', 'placeholder', '') if not os.path.exists(fname): self.config = config return config.read(fname) self.config = config LOGGER.info('[load config]') LOGGER.info('catalog address: %s', config['catalog']['address']) def save_config(self): if hasattr(self, 'inventory') and self.inventory: catalog = 'catalog' if not self.config.has_section(catalog): self.config.add_section(catalog) self.config.set(catalog, 'address', self.inventory.catalog_address) broker = 'broker' if not self.config.has_section(broker): self.config.add_section(broker) self.config.set(broker, 'address', self.inventory.broker_address) operator = 'operator' if not self.config.has_section(operator): self.config.add_section(operator) self.config.set(operator, 'address', self.operator_address if self.operator_address else '') self.config.set(operator, 'owner', self.account_id if self.account_id else '') if self.plugin and self.operator_address: fname = self.plugin.get_plugin_filename(self.operator_address) self.config.set(operator, 'solver_pluginfile', fname if fname else '') with open(CONFIG_INI_FILEPATH, 'w') as fout: self.config.write(fout) LOGGER.info('update config.') def load_misp_config(self, fname): # MISPに関する設定を設定ファイルから読み取る self.default_price = -1 self.default_quantity = -1 self.default_num_consign = -1 if not os.path.exists(fname): return config = configparser.ConfigParser() config.read(fname) try: self.default_price = config["MISP"].getint("defaultprice") self.default_quantity = config["MISP"].getint("defaultquantity") self.default_num_consign = config["MISP"].getint( "default_num_consign") LOGGER.info('[load MISP config]') except KeyError as err: LOGGER.warning('MISP configファイルの読み込みに失敗しました') LOGGER.warning(err) @staticmethod def uuid_to_filepath(uuid): return os.path.abspath('{}/{}.json'.format(MISP_DATAFILE_PATH, uuid)) @staticmethod def tokenaddress_to_filepath(address): return os.path.abspath('{}/{}'.format(FILESERVER_ASSETS_PATH, address)) def add_observer(self, observer): self.__observer = observer def notify_observer(self): self.__observer.update(self) @property def state(self): return self.__state @state.setter def state(self, state): self.__state = state # Stateと同じ名前の関数があれば自動実行 if state in dir(self): getattr(self, state)() self.notify_observer() def setup_inventory(self, catalog_address='', broker_address='', is_private=False): if catalog_address == '': catalog_address = self.contracts.accept(CTICatalog()).\ new(is_private).contract_address LOGGER.info('deployed CTICatalog. address: %s', catalog_address) if broker_address == '': broker_address = self.contracts.accept(CTIBroker()).\ new().contract_address LOGGER.info('deployed CTIBroker. address: %s', broker_address) if self.inventory: self.inventory.switch_catalog(catalog_address) self.inventory.switch_broker(broker_address) else: # inventory インスタンスの作成 self.inventory = Inventory(self.contracts, self.account_id, catalog_address, broker_address) self.save_config() def create_token(self, initial_supply, default_operators=None): # CTIトークンとなるERC777トークンの発行 ctitoken = self.contracts.accept(CTIToken()).new( initial_supply, default_operators if default_operators else []) return ctitoken.contract_address def accept_as_solver(self, view=None): LOGGER.info('accept as solver') if not self.inventory or not self.solver: return own_tokens = self.inventory.list_own_tokens(self.account_id) if len(own_tokens) == 0: return self.solver.accept_challenges(own_tokens, view=view) self.solver.reemit_pending_tasks() def setup_operator(self, operator_address='', solver_pluginfile='', view=None): if operator_address == self.operator_address: return if solver_pluginfile and \ not self.plugin.is_pluginfile(solver_pluginfile): raise Exception('invalid plugin file: ' + solver_pluginfile) old_operator_address = self.operator_address if operator_address == '': # オペレータのデプロイ ctioperator = self.contracts.accept(CTIOperator()) operator_address = ctioperator.new().contract_address ctioperator.set_recipient() if solver_pluginfile: self.plugin.set_solverclass(operator_address, solver_pluginfile) if operator_address != old_operator_address: if self.solver: self.solver.destroy() if operator_address: solverclass = self.plugin.get_solverclass(operator_address) self.solver = solverclass(self.contracts, self.account_id, operator_address) else: self.solver = None self.operator_address = operator_address self.save_config() if self.solver: self.accept_as_solver(view) def buy(self, token_address): # トークンの購買処理の実装 self.inventory.buy(token_address, allow_cheaper=True) def disseminate_token_from_mispdata(self, default_pirce, default_quantity, default_num_consign, view): # mispオブジェクトファイルの一覧をtokenとして公開する # 登録済みのtokenを取得 registered_token = self.fetch_registered_token() registered_uuid = [token.get('uuid') for token in registered_token] for obj_path in Path(MISP_DATAFILE_PATH).glob("./*.json"): # UUID (ファイル名から拡張子を省いた部分) を取得 uuid = obj_path.stem if uuid in registered_uuid: continue metadata = {} with open(obj_path) as fin: misp = json.load(fin) try: view.vio.print('disseminating CTI: \n' ' UUID: ' + uuid + '\n' ' TITLE: ' + misp['Event']['info'] + '\n') metadata['uuid'] = uuid metadata['title'] = misp['Event']['info'] metadata['price'] = default_pirce metadata['operator'] = self.operator_address metadata['quantity'] = default_quantity self.disseminate_new_token(metadata, default_num_consign) except KeyError: LOGGER.warning('There is no Event info in %s', misp) def disseminate_new_token(self, cti_metadata, num_consign=0): # ERC20/777 トークンを、独自トークンとして発行する # CTIトークンを作成 token_address = self.create_token(cti_metadata['quantity']) # カタログに登録して詳細をアップデート self.disseminate_token(token_address, cti_metadata) if num_consign > 0: self.inventory.consign(token_address, num_consign) return token_address def disseminate_token(self, token_address, cti_metadata): # トークンをカタログに登録 cti_metadata['tokenAddress'] = token_address self.create_asset_content(cti_metadata) self.register_catalog(token_address, cti_metadata) self.save_registered_token(cti_metadata) def create_asset_content(self, cti_metadata): misp_filepath = self.uuid_to_filepath(cti_metadata['uuid']) dist_linkpath = self.tokenaddress_to_filepath( cti_metadata['tokenAddress']) ## create a simple placeholder if MISP file does not exist. if not os.path.isfile(misp_filepath): LOGGER.warning('MISP file does not exist: %s', misp_filepath) os.makedirs(os.path.dirname(misp_filepath), exist_ok=True) ## simple placeholder with title. is this redundant? j = json.loads('{"Event":{"info": ""}}') j['Event']['info'] = cti_metadata['title'] with open(misp_filepath, 'w') as fout: json.dump(j, fout, indent=2, ensure_ascii=False) LOGGER.warning('created a simple placeholder. ' 'please overwrite the file above.') dist_dir = os.path.dirname(dist_linkpath) if not os.path.isdir(dist_dir): os.makedirs(dist_dir) LOGGER.warning('created missing directory for disseminate: %s', dist_dir) try: os.symlink(misp_filepath, dist_linkpath) except FileExistsError: LOGGER.error('disseminate link already exists: %s', dist_linkpath) def register_catalog(self, token_address, cti_metadata): self.inventory.register_token(self.account_id, token_address, cti_metadata) def unregister_catalog(self, token_address): self.inventory.unregister_token(token_address) def update_catalog(self, token_address, cti_metadata): self.inventory.modify_token(token_address, cti_metadata) @staticmethod def save_registered_token(cti_metadata): # cticatalog コントラクトに登録したtokenのmetadataを保存する fieldnames = [ 'uuid', 'tokenAddress', 'title', 'price', 'operator', 'quantity' ] is_empty = not os.path.isfile(REGISTERED_TOKEN_TSV) with open(REGISTERED_TOKEN_TSV, 'a', newline='') as tsvfile: writer = csv.DictWriter(tsvfile, fieldnames=fieldnames, extrasaction='ignore', delimiter='\t') if is_empty: writer.writeheader() writer.writerow(cti_metadata) @staticmethod def fetch_registered_token(): # 登録済みトークンのfetch registered_tokens = [] try: with open(REGISTERED_TOKEN_TSV, newline='') as tsvfile: tsv = csv.DictReader(tsvfile, delimiter='\t') for row in tsv: registered_tokens.append(row) return registered_tokens except FileNotFoundError: pass except Exception as err: LOGGER.error(err) return registered_tokens def consign(self, token_address, amount): self.inventory.consign(token_address, amount) def takeback(self, token_address, amount): self.inventory.takeback(token_address, amount) def watch_token_start(self, token_address, callback): ctitoken = self.contracts.accept(CTIToken()).get(token_address) argument_filters = dict() argument_filters['from'] = self.operator_address argument_filters['to'] = self.account_id event_filter = ctitoken.event_filter('Sent', fromBlock='latest', argument_filters=argument_filters) self.event_listener.add_event_filter('Sent:' + token_address, event_filter, callback) def watch_token_stop(self, token_address): self.event_listener.remove_event_filter_in_callback('Sent:' + token_address) def request_challenge(self, token_address, data=''): # token_address のトークンに対してチャレンジを実行 if self.seeker.challenge(self.operator_address, token_address, data=data): # トークン送付したので情報更新する self.inventory.update_balanceof_myself(token_address) @staticmethod def receive_challenge_answer(data): try: # data is generated at Solver.webhook(). download_url = data['download_url'] token_address = data['token_address'] if len(download_url) == 0 or len(token_address) == 0: raise Exception('received empty data') except: msg = '受信データの解析不能: ' + str(data) return False, msg msg = '' msg += '受信 URL: ' + download_url + '\n' msg += 'トークン: ' + token_address + '\n' try: request = Request(download_url, method="GET") with urlopen(request) as response: rdata = response.read() except Exception as err: LOGGER.error(err) msg += \ 'チャレンジ結果を受信しましたが、受信URLからの' + \ 'ダウンロードに失敗しました: ' + str(err) + '\n' msg += '手動でダウンロードしてください\n' return True, msg try: jdata = json.loads(rdata) title = jdata['Event']['info'] except: title = '(解析できませんでした)' msg += '取得データタイトル: ' + title try: if not os.path.isdir(DOWNLOADED_CTI_PATH): os.makedirs(DOWNLOADED_CTI_PATH) filepath = '{}/{}.json'.format(DOWNLOADED_CTI_PATH, token_address) with open(filepath, 'wb') as fout: fout.write(rdata) msg += '\n取得データを保存しました: ' + filepath except Exception as err: msg += '\n取得データの保存に失敗しました: ' + str(err) msg += '\n手動で再取得してください' return True, msg def cancel_challenge(self, task_id): assert self.operator_address self.seeker.cancel_challenge(self.operator_address, task_id) def fetch_task_id(self, token_address): token_related_task = self.contracts.accept(CTIOperator()).\ get(self.operator_address).history(token_address, MAX_HISTORY_NUM) # 最新の一つのみを表示 task_id = token_related_task[0] return task_id def fetch_trusted_users(self): # 信頼済みユーザのfetch trusted_users = [] try: with open(TRUSTED_USERS_TSV, newline='') as tsvfile: tsv = csv.DictReader(tsvfile, delimiter='\t') for row in tsv: try: row['id'] = Web3.toChecksumAddress(row['id']) except: continue if row['id'] == self.account_id: continue trusted_users.append(row['id']) self.trusted_users = trusted_users except FileNotFoundError: pass except Exception as err: LOGGER.error(err) def interest_assets(self): if not self.interest: return self.inventory.catalog_tokens filtered_assets = filter(lambda x: self.interest in x[1]['title'], self.inventory.catalog_tokens.items()) return dict(filtered_assets) def accept_challenge(self, token_address, view=None): LOGGER.info('accept_challenge token: %s', token_address) self.solver.accept_challenges([token_address], view=view) def refuse_challenge(self, token_address): LOGGER.info('refuse_challenge token: %s', token_address) self.solver.refuse_challenges([token_address]) def like_cti(self, token_address): assert self.inventory self.inventory.like_cti(token_address) def get_like_users(self): try: return self.inventory.like_users except: return dict() def send_token(self, token_address, target_address, amount): assert token_address and target_address and amount > 0 try: self.contracts.accept(CTIToken()).get(token_address).\ send_token(target_address, amount) # ブローカー経由でないためイベントは飛ばない。手動で反映する。 self.inventory.update_balanceof_myself(token_address) return True except Exception as err: LOGGER.exception(err) return False def burn_token(self, token_address, amount, data=''): assert token_address and amount > 0 try: self.contracts.accept(CTIToken()).get(token_address).\ burn_token(amount, data) # Burned イベントが飛ぶがキャッチしていない。手動で反映する。 self.inventory.update_balanceof_myself(token_address) return True except Exception as err: LOGGER.exception(err) return False