def _ParseChannel(channel): """Parse a single messy definition of a channel into a Channel proto. Parsing: * If a Channel, return as is. * If a string, convert into a public channel with id equal to the string. * If a dictionary or HypeParams object, convert into a public channel with the matching fields set. Args: channel: The messy object to convert into a channel. Returns: Channel or None if it failed to parse. """ if isinstance(channel, channel_pb2.Channel): return channel if isinstance(channel, HypeParams): channel = channel.AsDict() if isinstance(channel, dict): try: return channel_pb2.Channel(visibility=channel_pb2.Channel.PUBLIC, **channel) except KeyError: logging.error('Failed to parse %s as a Channel', channel) return None if isinstance(channel, six.string_types): return channel_pb2.Channel(visibility=channel_pb2.Channel.PUBLIC, id=channel, name=channel) return None
def _ExtractOverrides(self, raw_message): """Returns nothing, or the overrides parsed for a channel, user, and msg.""" message_regex = re.compile(r'^\[(#?\w+)(?:\|(#?\w+))?\]\s*(.+)') match = message_regex.match(raw_message) if not match: return username = self._params.default_username channel_name = self._params.default_channel.name visibility = channel_pb2.Channel.PUBLIC for i in range(1, 3): user_or_channel = match.group(i) if not user_or_channel: break if user_or_channel.startswith('#'): channel_name = user_or_channel if channel_name.startswith('#sys'): visibility = channel_pb2.Channel.SYSTEM else: username = user_or_channel if username.lower() == channel_name.strip('#').lower(): visibility = channel_pb2.Channel.PRIVATE message = match.group(3) return (channel_pb2.Channel(id=channel_name, visibility=visibility, name=channel_name), user_pb2.User(user_id=username, display_name=username, bot=username.endswith('bot')), message)
def PublishMessage(self, topic: Text, msg: hype_types.CommandResponse, notice: bool = False) -> None: """Sends a message to the channels subscribed to a topic. Args: topic: Name of the topic on which to publish the message. msg: The message to send. notice: If true, use interface.Notice instead of interface.SendMessage. """ if not msg: return if not topic: logging.warning('Attempted to publish message with no topic: %s', msg) return channels = self.params.subscriptions.get(topic, []) if not channels: logging.info('No subscriptions for topic %s, dropping: %s', topic, msg) return message = util_lib.MakeMessage(msg) for channel in channels: channel = channel_pb2.Channel( visibility=channel_pb2.Channel.PUBLIC, **channel) if notice: self.interface.Notice(channel, message) else: self.interface.SendMessage(channel, message)
def _Ratelimit(self, channel: channel_pb2.Channel, user: user_pb2.User, *args, **kwargs): """Ratelimits calls/responses from Handling the message. In general this, prevents the same user, channel, or global triggering a command in quick succession. This works by timing calls and verifying that future calls have exceeded the interval before invocation. Some commands like to handle every message, but only respond to a few. E.g., MissingPing needs every message to record the most recent user, but only sends a response when someone types '?'. In this case, we always execute the command and only ratelimit the response. The restriction being, that it is safe to call on every invocation. E.g., do not transfer hypecoins. Args: channel: Passed through to _Handle. user: Passed through to _Handle. *args: Passed through to _Handle. **kwargs: Passed through to _Handle. Returns: Optional message(s) to reply to the channel. """ if (not self._params.ratelimit.enabled or channel.visibility in [ channel_pb2.Channel.PRIVATE, channel_pb2.Channel.SYSTEM ]): return self._Handle(channel, user, *args, **kwargs) scoped_channel = channel_pb2.Channel() scoped_channel.CopyFrom(channel) scoped_user_id = user.user_id if self._params.ratelimit.scope == 'GLOBAL': scoped_channel.id = self._DEFAULT_SCOPE scoped_user_id = self._DEFAULT_SCOPE elif self._params.ratelimit.scope == 'CHANNEL': scoped_user_id = self._DEFAULT_SCOPE with self._ratelimit_lock: t = time.time() delta_t = t - self._last_called[scoped_channel.id][scoped_user_id] response = None if self._params.ratelimit.return_only: response = self._Handle(channel, user, *args, **kwargs) if not response: return if delta_t < self._params.ratelimit.interval: logging.info('Call to %s._Handle ratelimited in %s for %s: %s < %s', self.__class__.__name__, scoped_channel.id, scoped_user_id, delta_t, self._params.ratelimit.interval) self._Reply(user, random.choice(messages.RATELIMIT_MEMES)) return self._last_called[scoped_channel.id][scoped_user_id] = t return response or self._Handle(channel, user, *args, **kwargs)
async def on_message(message): # Discord doesn't protect us from responding to ourself. if message.author == self._client.user: return logging.info('Message from: %s - %s#%s - %s', message.author.name, message.author.display_name, message.author.discriminator, message.author.id) user = user_pb2.User(user_id=str(message.author.id), display_name=message.author.display_name) # Discord has DMChannel for single user interaction and GroupChannel for # group DMs outside of traditional TextChannels within a guild. We only # consider the DMChannel (single user) as private to prevent spam in Group # conversations. if isinstance(message.channel, discord.DMChannel): channel = channel_pb2.Channel( id=str(message.channel.id), visibility=channel_pb2.Channel.PRIVATE, name=message.channel.recipient.name) else: channel = channel_pb2.Channel( id=str(message.channel.id), visibility=channel_pb2.Channel.PUBLIC, name=message.channel.name) self._on_message_fn(channel, user, self._CleanContent(message))
def _ProcessNestedCalls(self, channel, user, msg): """Evaluate nested commands within $(...).""" m = self.NESTED_PATTERN.search(msg) while m: backup_interface = self._core.interface self._core.interface = interface_factory.Create( 'CaptureInterface', {}) # Pretend it's Private to avoid ratelimit. nested_channel = channel_pb2.Channel( id=channel.id, visibility=channel_pb2.Channel.PRIVATE, name=channel.name) self.HandleMessage(nested_channel, user, m.group(1)) response = self._core.interface.MessageLog() msg = msg[:m.start()] + response + msg[m.end():] self._core.interface = backup_interface m = self.NESTED_PATTERN.search(msg) return msg
from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import unittest from hypebot import basebot from hypebot import hypecore from hypebot.core import params_lib from hypebot.interfaces import interface_factory from hypebot.protos import channel_pb2 from hypebot.protos import user_pb2 TEST_CHANNEL = channel_pb2.Channel(id='#test', name='Test', visibility=channel_pb2.Channel.PUBLIC) TEST_USER = user_pb2.User(user_id='_test', display_name='user') def ForCommand(command_cls): """Decorator to enable setting the command for each test class.""" def _Internal(test_cls): test_cls._command_cls = command_cls return test_cls return _Internal class BaseCommandTestCase(unittest.TestCase):