blob: c96e4271b40c90aa167c30503a775957ed248367 [file] [log] [blame]
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import re
import socket
import time
# This is the maximum size of an IRC message.
BUFFER_SIZE = 512
# Some hosts take several seconds to send MOTD and joining the channel.
TIMEOUT = 30
# Try to connect this many times before giving up.
RETRIES = 3
class IRCClient(object):
"""A numb IRC client to connect and send a message to a IRC channel."""
def __init__(self,
server_hostname,
channel_name,
nick_name,
description,
port=6667):
self._server_hostname = server_hostname
self._channel_name = channel_name
self._nick_name = nick_name
self._description = description
self._port = port
self._joined = False
self._irc = None
self._timeout = 0
self._retries_left = RETRIES
def Connect(self):
"""Opens a connection to the irc server and joins a channel."""
assert self._irc is None, 'Already connected'
self._irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._irc.connect((self._server_hostname, self._port))
self._irc.settimeout(TIMEOUT)
self._irc.sendall('USER {name} {name} {name} : {description}\r\n'.format(
name=self._nick_name, description=self._description))
self._irc.sendall('NICK %s\r\n' % self._nick_name)
self._irc.sendall('JOIN %s\r\n' % self._channel_name)
self._WaitForJoin()
self._joined = True
logging.info('Joined %s on %s', self._channel_name, self._server_hostname)
def _WaitForJoin(self):
"""Waits for confirmation that we joined the channel.
If we send a message before the sender confirms we joined the channel, the
message may be dropped.
"""
join_message_regex = re.compile(r'[^ ]+ [^ ]+ %s @ %s.*' %
(self._nick_name, self._channel_name))
# Keep the last line in this var in case it is truncated.
partial_line = ''
while True:
messages = partial_line + self._irc.recv(BUFFER_SIZE)
# Loop through the messages until we find confirmation of joining the
# channel.
for message in messages.splitlines(True): # Keep '\n' or '\r' suffix.
if message[-1] not in '\n\r':
partial_line = message
else:
partial_line = ''
if join_message_regex.match(message.strip()):
return
def SendMessage(self, message, receiver=None, retries=3, retry_delay=5):
"""Sends a message to the joined channel, or specific user.
Args:
message(str): A short message (suported length depends on host).
receiver(str): Nick of receiver. None if the target is the channel.
retries(int) (optional): How may times to retry sending the message if an
unexpected socket exception or timeout happens. Default 3.
retry_delay(float) (optional): How many seconds to sleep between the first
and second attempts at sending the message. This will be doubled for
each attempt. Default 5 sec.
"""
assert self._joined, 'Not joined yet!'
# Even with retries == 0, we still need to go into the loop once.
tries_left = max(retries + 1, 1) # Force a positive retry counter.
while tries_left > 0: # pragma: no branch.
try:
receiver = receiver or self._channel_name
self._irc.sendall('PRIVMSG {receiver} :{message}\r\n'.format(
receiver=receiver, message=message))
logging.info('Message sent to %s: %s', receiver, message)
return
except (socket.timeout, IOError) as e:
if tries_left:
logging.warning('Failed to send messsage due to %s, '
'retrying after %s seconds', e, retry_delay)
time.sleep(retry_delay)
tries_left -= 1
retry_delay *= 2
else:
logging.exception(
'Exceeded %d retries sending irc message. Giving up', retries)
raise
def Disconnect(self):
"""Leaves the channel if needed, then closes connection to server."""
if self._joined:
self._irc.sendall('PART %s\r\n' % self._channel_name)
self._irc.sendall('QUIT\r\n')
logging.info('Leave %s, and quit', self._channel_name)
# There's not much benefit in testing this branch.
if self._irc: # pragma: no branch
self._irc.close()
def __enter__(self):
while True:
try:
self.Connect()
return self
except (socket.timeout, IOError):
logging.debug(
'Did not get irc join confirmation message, connecting again.')
self._retries_left -= 1
self.Disconnect()
self._irc = None
if self._retries_left < 1:
raise
def __exit__(self, _exc_type, _exc_val, _exc_tb):
self.Disconnect()