Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@activitycentral.org>2011-04-26 11:25:17 (GMT)
committer Aleksey Lim <alsroot@activitycentral.org>2011-04-26 11:25:17 (GMT)
commit74024d9d8ff4442f51d7178418e9b041af0809eb (patch)
tree01d8f70b500829659b52702a8c106ba722ab8c54
Extract from puppet module
-rw-r--r--__init__.py52
-rw-r--r--config.py53
-rw-r--r--lingva_bot.py76
-rw-r--r--plugin.py603
-rw-r--r--test.py106
5 files changed, 890 insertions, 0 deletions
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..efa0f04
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2010, Aleksey Lim
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions, and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions, and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the author of this software nor the name of
+# contributors to this software may be used to endorse or promote products
+# derived from this software without specific prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""
+Lingvo is a language in Esperanto.
+Online translation on IRC.
+"""
+
+import supybot
+import supybot.world as world
+
+__version__ = '1'
+__author__ = supybot.Author('Aleksey Lim', 'alsroot', 'alsroot@sugarlabs.org')
+__contributors__ = {}
+
+import lingva_bot
+import config
+import plugin
+
+# In case we're being reloaded.
+reload(lingva_bot)
+reload(config)
+reload(plugin)
+
+if world.testing:
+ import test
+
+Class = plugin.Class
+configure = config.configure
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..db4ecc4
--- /dev/null
+++ b/config.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2010, Aleksey Lim
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions, and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions, and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the author of this software nor the name of
+# contributors to this software may be used to endorse or promote products
+# derived from this software without specific prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import supybot.conf as conf
+import supybot.registry as registry
+
+from lingva_bot import *
+
+
+def configure(advanced):
+ conf.registerPlugin('Lingvo', True)
+
+
+Lingvo = conf.registerPlugin('Lingvo')
+
+conf.registerGlobalValue(Lingvo, 'timeout',
+ registry.PositiveInteger(60 * 60 * 24 * 3,
+ 'Timeout in seconds before closing empty translation channels'))
+conf.registerGlobalValue(Lingvo, 'contact',
+ registry.String('meeting@sugarlabs.org',
+ 'Administrative contant'))
+conf.registerGlobalValue(Lingvo, 'documentation_url',
+ registry.String('http://wiki.sugarlabs.org/go/Service/meeting/Usage',
+ 'Url to the site with documentations'))
+conf.registerChannelValue(Lingvo, 'pinned',
+ registry.Boolean(False,
+ 'Should empty translation channels be auto closed'))
+conf.registerChannelValue(Lingvo, 'blacklist',
+ conf.SpaceSeparatedSetOfChannels([],
+ 'List of channels that should not be translation channels'))
diff --git a/lingva_bot.py b/lingva_bot.py
new file mode 100644
index 0000000..cc8c904
--- /dev/null
+++ b/lingva_bot.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+LINGVA_BOT = 'meeting'
+
+CHANNELS = [
+ ('', '', 'General Sugar channels'),
+ ('en', '#sugar', 'Common Sugar discussions'),
+ ('en', '#sugar-newbies', 'New Sugar developer help'),
+ ('en', '#sugar-meeting', 'Handling Sugar meetings'),
+ ('', '', 'Sugar distributions'),
+ ('en', '#dextrose', 'Sugar distribution for XO laptops'),
+ ('en', '#fedora-olpc', 'The home of the Fedora interest group for OLPC'),
+ ('en', '#olpc-help', 'Community help, if you need help using your XO'),
+ ('es', '#olpc-paraguay', 'Paraguay Educa'),
+ ('en', '#ubuntu-sugarteam', 'Ubuntu Sugar Team'),
+ ('', '', 'Community channels'),
+ ('en', '#etoys', 'Etoys project'),
+ ('en', '#squeak', 'Squeak Smalltalk'),
+ ('en', '#ubuntu-us-dc', 'Washington DC Ubuntu local team'),
+ ('', '', 'Other'),
+ ('en', '#meeting-test', 'Test meeting bot'),
+ ]
+
+LANGUAGES = [
+ ('af', 'Afrikaans', ""),
+ ('ar', 'عربي', ""),
+ ('be', 'Беларуская', ""),
+ ('bg', 'Български', ""),
+ ('ca', 'català', ""),
+ ('cs', 'Čeština', ""),
+ ('cy', 'Cymraeg', ""),
+ ('da', 'Dansk', ""),
+ ('de', 'Deutsch', ""),
+ ('el', 'Ελληνικά', ""),
+ ('en', 'English', "This is a mirror of %(src_channel)s. What you write in %(dst_lang)s here, will be translated into %(src_lang)s, then posted to %(src_channel)s, and vice versa. %(docs)s"),
+ ('es', 'Español', "Este es un espejo de %(src_channel)s. Lo que usted escribe en %(dst_lang)s aquí, se traducirá en %(src_lang)s, a continuación, envió a %(src_channel)s, y viceversa. %(docs)s"),
+ ('et', 'Eesti keel', ""),
+ ('fa', 'فارسی', ""),
+ ('fi', 'suomi', ""),
+ ('fr', 'Français', ""),
+ ('ga', 'Gaeilge', ""),
+ ('gl', 'Galego', ""),
+ ('hi', 'हिन्दी', ""),
+ ('hr', 'Hrvatski', ""),
+ ('ht', 'Kreyòl ayisyen', ""),
+ ('hu', 'Magyar', ""),
+ ('id', 'Bahasa Indonesia', ""),
+ ('is', 'íslenska', ""),
+ ('it', 'Italiano', ""),
+ ('iw', 'עברית', ""),
+ ('ja', '日本語', ""),
+ ('ko', '한국어', ""),
+ ('lt', 'lietuvių kalba', ""),
+ ('lv', 'Latviešu', ""),
+ ('mk', 'Македонски', ""),
+ ('ms', 'بهاس ملايو', ""),
+ ('mt', 'Malti', ""),
+ ('nl', 'Nederlands', ""),
+ ('no', 'Norsk', ""),
+ ('pl', 'Polski', ""),
+ ('pt', 'Português (do Brasil)', ""),
+ ('pt-PT', 'Português (Europeu)', ""),
+ ('ro', 'română', ""),
+ ('ru', 'Русский', "Это зеркало автоматического перевода для %(src_channel)s. Всё, что вы пишите здесь по-русски, будет переведено на %(src_lang)s и отправлено в %(src_channel)s. Тоже самое в обратном направлении. %(docs)s"),
+ ('sk', 'slovenčina', ""),
+ ('sl', 'slovensko', ""),
+ ('sq', 'Shqip', ""),
+ ('sr', 'Српски', ""),
+ ('sv', 'Svenska', ""),
+ ('sw', 'Swahili', ""),
+ ('th', 'ไทย', ""),
+ ('tl', 'Tagalog', ""),
+ ('tr', 'Türkçe', ""),
+ ('uk', 'Українська', ""),
+ ('vi', 'Tiếng Việt', ""),
+ ]
diff --git a/plugin.py b/plugin.py
new file mode 100644
index 0000000..2496a3e
--- /dev/null
+++ b/plugin.py
@@ -0,0 +1,603 @@
+# Copyright (C) 2010, Aleksey Lim
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions, and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions, and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the author of this software nor the name of
+# contributors to this software may be used to endorse or promote products
+# derived from this software without specific prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import time
+import json
+import urllib
+import htmllib
+
+from supybot.commands import *
+import supybot.irclib as irclib
+import supybot.ircmsgs as ircmsgs
+import supybot.callbacks as callbacks
+import supybot.utils as utils
+import supybot.world as world
+import supybot.ircdb as ircdb
+import supybot.conf as conf
+
+import config
+
+
+_TRANSLATE_URL = 'http://ajax.googleapis.com/ajax/services/language/translate'
+_QUOTES = "\""
+
+_LANGUAGE_CODES = [i[0] for i in config.LANGUAGES]
+
+
+class LingvaGroup(dict):
+ channel = None
+ lang = None
+ topic = None
+
+
+class Lingvo(callbacks.Plugin):
+
+ def __init__(self, irc):
+ callbacks.Plugin.__init__(self, irc)
+
+ self._irc = irc
+ self.lastMsgs = {}
+ self.lastStates = {}
+ self._auto_parts = {}
+ self._groups_value = None
+
+ @property
+ def _groups(self):
+ if self._groups_value is None:
+ self._groups_value = {}
+
+ for lang, name, __ in config.CHANNELS:
+ if not lang:
+ continue
+
+ group = LingvaGroup()
+ group.lang = lang
+ group.channel = name
+
+ for i in self._irc.state.channels:
+ group_channel, lang = _normalize_channel(i)
+ if group_channel == group.channel and lang:
+ group[i] = lang
+
+ self._groups_value[name] = group
+
+ return self._groups_value
+
+ def __call__(self, irc, msg):
+ try:
+ callbacks.Plugin.__call__(self, irc, msg)
+ if irc in self.lastMsgs:
+ if irc not in self.lastStates:
+ self.lastStates[irc] = irc.state.copy()
+ self.lastStates[irc].addMsg(irc, self.lastMsgs[irc])
+ finally:
+ # We must make sure this always gets updated.
+ self.lastMsgs[irc] = msg
+
+ def cmd_join(self, irc, msg, args, optlist, channel, lang):
+ """[--silent] [<channel>] [<lang>]
+
+ Starts relaying between specified (or current one if <channel> is
+ omitted) and newly joined, satellite, channel of specified language.
+ If language is omitted, it will be parsed from channel name.
+ """
+ if lang:
+ lang = lang.lower()
+ else:
+ __, lang = _normalize_channel(channel)
+ if not lang or lang not in _LANGUAGE_CODES:
+ all_langs = []
+ for code, name, __ in config.LANGUAGES:
+ all_langs.append('%s(%s)' % (code, name))
+ irc.error('Wrong "%s" language, use one of %s' % \
+ (lang, ', '.join(all_langs)))
+ return
+
+ silent = False
+
+ for (option, arg) in optlist:
+ if option == 'silent':
+ silent = True
+
+ group_channel, __ = _normalize_channel(channel)
+ if group_channel not in self._groups:
+ irc.error('Channel "%s" cannot be translated, ' \
+ 'request it using %s contact' % \
+ (group_channel, self.registryValue('contact')))
+ return
+
+ group = self._groups[group_channel]
+
+ if lang == group.lang:
+ irc.error('Channel %s has the same default language' % \
+ group_channel)
+ return
+
+ if channel in group:
+ lingva_channel = channel
+ else:
+ lingva_channel = '%s-%s' % (group_channel, lang)
+
+ if lingva_channel in self.registryValue('blacklist'):
+ irc.error('Channel "%s" is a regular channel and cannot be used ' \
+ 'for translation. If it is not true any more, please ' \
+ 'email to %s.' % \
+ (lingva_channel, self.registryValue('contact')))
+ return
+
+ if lingva_channel not in irc.state.channels:
+ network = conf.supybot.networks.get(irc.network)
+ network.channels().add(lingva_channel)
+ irc.sendMsg(network.channels.join(lingva_channel))
+
+ if not silent:
+ irc.reply('Follow me on %s' % lingva_channel)
+
+ join = wrap(cmd_join,
+ [getopts({'silent': ''}), 'channel', optional('text')])
+
+ def cmd_part(self, irc, msg, args, optlist, channel, lang):
+ """[--silent] [<channel>] [<lang>|all]
+
+ Close current translation channel or translation channel(s) for
+ specified language.
+ """
+ if lang:
+ lang = lang.lower()
+
+ silent = False
+
+ for (option, arg) in optlist:
+ if option == 'silent':
+ silent = True
+
+ for group in self._groups.values():
+ if channel not in group and channel != group.channel:
+ continue
+
+ left = []
+
+ for i in group.keys():
+ if lang == 'all' or lang == group[i] or \
+ not lang and channel == i:
+ irc.queueMsg(ircmsgs.part(i, 'Lingvo part'))
+ del group[i]
+ left.append(i)
+
+ if not left:
+ break
+ if not silent:
+ irc.reply('Has left %s channel(s)' % ', '.join(left))
+ return
+
+ if not silent:
+ irc.error('Cannot find translation channels')
+
+ part = wrap(cmd_part, [getopts({'silent': ''}), 'channel',
+ optional('text'), 'admin'])
+
+ def cmd_list(self, irc, msg, args, channel):
+ """[<channel>]
+
+ List active translation languages for specified primal channel(s).
+ """
+ if channel:
+ if not channel.startswith('#'):
+ channel = '#' + channel
+ channel, __ = _normalize_channel(channel)
+
+ for group in sorted(self._groups.values(),
+ cmp=lambda x, y: cmp(x.channel, y.channel)):
+ if channel and channel != group.channel:
+ continue
+ if group:
+ irc.reply('%s is being translated into %s' % \
+ (group.channel, ', '.join(group.values())))
+ else:
+ irc.reply(group.channel)
+
+ list = wrap(cmd_list, [optional('text')])
+
+ def cmd_langs(self, irc, msg, args):
+ """
+ List of supported languages.
+ """
+ langs = ['%s %s' % (code, name) for code, name, __ in config.LANGUAGES]
+ irc.reply(', '.join(langs))
+
+ langs = wrap(cmd_langs, [])
+
+ def cmd_pin(self, irc, msg, args, channel):
+ """[<channel>]
+
+ Prevent auto closing empty translation channels.
+ """
+ group_channel, lang = _normalize_channel(channel)
+ if not lang:
+ irc.reply('Not translation channel connot be pinned')
+ return
+
+ self.setRegistryValue('pinned', True, channel)
+ irc.replySuccess()
+
+ pin = wrap(cmd_pin, ['channel'])
+
+ def cmd_unpin(self, irc, msg, args, channel):
+ """[<channel>]
+
+ Allow auto closing empty translation channels.
+ """
+ group_channel, lang = _normalize_channel(channel)
+ if not lang:
+ irc.reply('Not translation channel connot be pinned')
+ return
+
+ self.setRegistryValue('pinned', False, channel)
+ irc.replySuccess()
+
+ unpin = wrap(cmd_unpin, ['channel'])
+
+ def cmd_nicks(self, irc, msg, args, channel):
+ """[<channel>]
+
+ List all nicks that take part in conversation, i.e., nicks from
+ the primal channel and nicks from all translation channels.
+ """
+ group_channel, lang = _normalize_channel(channel)
+ if group_channel not in self._groups:
+ irc.reply('Channel %s is not translated' % group_channel)
+ return
+
+ nicks = irc.state.channels[group_channel].users
+ for channel, lang in self._groups[group_channel].items():
+ nicks |= irc.state.channels[channel].users
+
+ irc.reply(', '.join(sorted(nicks)))
+
+ nicks = wrap(cmd_nicks, ['channel'])
+
+ def doPrivmsg(self, irc, msg):
+ if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg) or \
+ msg.nick == irc.nick:
+ return
+
+ group = self._relay(irc, msg) or {}
+
+ for channel in group.keys():
+ if self.registryValue('pinned', group.channel) or \
+ self.registryValue('pinned', channel):
+ continue
+ if len(irc.state.channels[channel].users) <= 1:
+ time_last = self._auto_parts.get(channel)
+ time_current = int(time.time())
+ if time_last is None:
+ self._auto_parts[channel] = time_current
+ elif time_current - time_last >= self.registryValue('timeout'):
+ irc.queueMsg(ircmsgs.part(channel, 'Lingvo part'))
+ elif channel in self._auto_parts:
+ del self._auto_parts[channel]
+
+ def doJoin(self, irc, msg):
+ if msg.nick != irc.nick:
+ self._relay(irc, msg, 'has joined', skip_joined_users=True)
+ else:
+ channel = msg.args[0]
+ group_channel, lang = _normalize_channel(channel)
+ group = self._groups.get(group_channel)
+
+ if group is not None and lang:
+ group[channel] = lang
+
+ for code, name, topic in config.LANGUAGES:
+ if code == lang:
+ topic_native = topic
+ dst_lang_name = name
+ if code == 'en':
+ topic_en = topic
+ if code == group.lang:
+ src_lang_name = name
+
+ topic_args = {'src_channel': group.channel,
+ 'dst_channel': channel,
+ 'src_lang': src_lang_name,
+ 'dst_lang': dst_lang_name,
+ 'docs': self.registryValue('documentation_url'),
+ }
+
+ if topic_native:
+ group.topic = topic_native % topic_args
+ else:
+ group.topic = self._translate(
+ topic_en % topic_args, group.lang, lang)
+ if not group.topic:
+ group.topic = topic_en % topic_args
+
+ irc.queueMsg(ircmsgs.topic(channel, group.topic))
+
+ text = 'Translation has started for'
+ msg_format = '* %%s %s' % group_channel
+ self._relay_to(irc, channel, msg_format, text, 'en', lang)
+
+ def doMode(self, irc, msg):
+ if msg.nick != irc.nick:
+ return
+
+ channel = msg.args[0]
+ group = self._groups.get(channel)
+
+ if group is not None:
+ irc.queueMsg(ircmsgs.topic(channel, group.topic))
+
+ def doPart(self, irc, msg):
+ if msg.nick != irc.nick:
+ if len(msg.args) == 1:
+ text = 'has left'
+ else:
+ text = 'has left (%s)' % msg.args[1]
+ self._relay(irc, msg, text, skip_joined_users=True)
+ else:
+ channel = msg.args[0]
+ group_channel, lang = _normalize_channel(channel)
+ group = self._groups.get(group_channel)
+
+ if group is not None and channel in group:
+ network = conf.supybot.networks.get(irc.network)
+ if channel in network.channels():
+ network.channels().remove(channel)
+ del group[channel]
+
+ def doKick(self, irc, msg):
+ if len(msg.args) == 3:
+ text = 'kicked %s (%s)' % (msg.args[1], msg.args[2])
+ else:
+ text = 'kicked %s' % msg.args[1]
+ self._relay(irc, msg, text)
+
+ def doTopic(self, irc, msg):
+ text = 'changed topic to %s' % msg.args[1]
+ self._relay(irc, msg, text)
+
+ def outFilter(self, irc, msg):
+ if msg.command == 'PRIVMSG' and not msg.relayed and \
+ msg.nick != irc.nick:
+ self._relay(irc, msg)
+ return msg
+
+ def doNick(self, irc, msg):
+ new_nick = msg.args[0]
+ msg_format = '* %s %%s %s' % (msg.nick, new_nick)
+ text = 'nick changed to'
+
+ self._relay_farewell(irc, irc.state.channels,
+ new_nick, msg_format, text)
+
+ def doQuit(self, irc, msg):
+ if msg.args:
+ text = 'has quit (%s)' % msg.args[0]
+ else:
+ text = 'has quit'
+ msg_format = '* %s %%s' % msg.nick
+
+ if not isinstance(irc, irclib.Irc):
+ irc = irc.getRealIrc()
+
+ if irc not in self.lastStates:
+ return
+
+ self._relay_farewell(irc, self.lastStates[irc].channels, msg.nick,
+ msg_format, text)
+
+ def _translate(self, text, src_lang, dst_lang, nicks=None):
+ if src_lang == dst_lang:
+ return text
+
+ quoted_text, replaces = quote(text, nicks)
+ if not unquote(quoted_text, replaces, '').strip():
+ return text
+
+ url_opts = {
+ 'v': '1.0',
+ 'q': quoted_text,
+ 'langpair': '%s|%s' % (src_lang, dst_lang),
+ }
+ url = '%s?%s' % (_TRANSLATE_URL, urllib.urlencode(url_opts))
+
+ fd = utils.web.getUrlFd(url)
+ reply = json.load(fd)
+ fd.close()
+
+ if reply['responseStatus'] != 200:
+ self.log.warn('Bad Google resonce for "%s"' % url)
+ else:
+ quoted_text = _unescape(reply['responseData']['translatedText'])
+ return unquote(quoted_text, replaces)
+
+ def _relay_to(self, irc, channel, msg_format, text, src_lang, dst_lang,
+ nicks=None):
+ text = self._translate(text, src_lang, dst_lang, nicks)
+ if not text:
+ return
+
+ msg = ircmsgs.privmsg(channel, msg_format % text)
+
+ assert msg.command in ('PRIVMSG', 'NOTICE', 'TOPIC')
+
+ msg.tag('relayed')
+ irc.queueMsg(msg)
+
+ def _relay_farewell(self, irc, channels, nick, msg_format, text):
+ for group in self._groups.values():
+ if not [i for i in group.keys() + [group.channel] \
+ if i in channels and nick in channels[i].users]:
+ continue
+ for channel, lang in group.items() + [(group.channel, group.lang)]:
+ irc_channel = channels.get(channel)
+ if irc_channel is None or len(irc_channel.users) == 1 or \
+ nick in irc_channel.users:
+ continue
+ self._relay_to(irc, channel, msg_format, text, 'en', lang)
+
+ def _relay(self, irc, msg, text=None, skip_joined_users=False):
+ nick = msg.nick or irc.nick
+ if nick == irc.nick:
+ return
+
+ src_channel = msg.args[0]
+ if not src_channel:
+ return
+
+ group_channel, src_lang = _normalize_channel(src_channel)
+ group = self._groups.get(group_channel)
+
+ if group is None:
+ return
+
+ if not src_lang:
+ src_lang = group.lang
+
+ if text is None:
+ if ircmsgs.isAction(msg):
+ text = ircmsgs.unAction(msg)
+ else:
+ text = msg.args[1]
+ if text.startswith(irc.nick + ':'):
+ return
+
+ if msg.command == 'PRIVMSG':
+ msg_format = '<%s-%s> %%s' % (nick, src_lang)
+ else:
+ msg_format = '* %s-%s %%s' % (nick, src_lang)
+ src_lang = 'en'
+
+ group_nicks = irc.state.channels[group_channel].users
+
+ for dst_channel, dst_lang in \
+ group.items() + [(group.channel, group.lang)]:
+ irc_channel = irc.state.channels[dst_channel]
+ if irc_channel is None or dst_channel == src_channel or \
+ len(irc_channel.users) == 1 or \
+ (skip_joined_users and nick in irc_channel.users):
+ continue
+ nicks = group_nicks | irc.state.channels[src_channel].users | \
+ irc_channel.users
+ self._relay_to(irc, dst_channel, msg_format, text,
+ src_lang, dst_lang, nicks)
+
+ return group
+
+
+def quote(text, ext_replaces=None):
+ replaces = {}
+ new_text = ''
+ next_key_value = [1]
+
+ def next_key():
+ key = '%03d' % next_key_value[0]
+ next_key_value[0] += 1
+ return key
+
+ while text:
+ start_quote = None
+
+ for i in _QUOTES + '#':
+ if i in text:
+ start_quote = i
+ break
+
+ if start_quote is None:
+ new_text += text
+ break
+
+ start = text.index(start_quote)
+
+ key = next_key()
+ new_text += text[:start] + key
+
+ if start_quote == '#':
+ end = start + 1
+ while end < len(text) and not text[end].isspace():
+ end += 1
+ end -= 1
+ else:
+ end = text.find(start_quote, start + 1)
+ if end == -1:
+ end = len(text)
+
+ replaces[key] = text[start:end + 1]
+ text = text[end + 1:]
+
+ for value in sorted(ext_replaces or [],
+ cmp=lambda x, y: cmp(len(y), len(x))):
+ text = new_text
+ new_text = ''
+ new_text_pos = 0
+
+ for match in re.finditer(
+ r'(^|(?<=[\s:,]))(%s)(-([a-zA-Z-]+))*($|(?=[\s:,]))' % value,
+ text):
+ match_value = match.groups()[1]
+ match_lang_postfix = match.groups()[2]
+ match_lang = match.groups()[3]
+ if match_lang and match_lang not in _LANGUAGE_CODES:
+ match_value += match_lang_postfix
+
+ key = next_key()
+ replaces[key] = match_value
+ new_text += text[new_text_pos:match.start()] + key
+ new_text_pos = match.end()
+
+ new_text += text[new_text_pos:]
+
+ return (new_text, replaces)
+
+
+def unquote(text, replaces, stub=None):
+ for key, value in replaces.items():
+ text = text.replace(key, value if stub is None else stub)
+ return text
+
+
+def _unescape(text):
+ parser = htmllib.HTMLParser(None)
+ parser.save_bgn()
+ parser.feed(text.encode('utf-8'))
+ return parser.save_end()
+
+
+def _normalize_channel(name):
+ match = re.match('(.*)-([-A-Za-z]+)$', name)
+
+ if match:
+ normalized_name = match.groups()[0]
+ lang = match.groups()[1]
+ if lang in _LANGUAGE_CODES:
+ return (normalized_name, lang)
+
+ return (name, None)
+
+
+Class = Lingvo
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..aee97f1
--- /dev/null
+++ b/test.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2010, Aleksey Lim
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions, and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions, and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the author of this software nor the name of
+# contributors to this software may be used to endorse or promote products
+# derived from this software without specific prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from supybot.test import *
+
+import plugin
+
+
+class LingvoTestCase(PluginTestCase):
+ plugins = ('Lingvo', 'Utilities')
+
+ def testFoo(self):
+ self.assertResponse('echo foo', 'foo')
+
+ def testQuoting(self):
+ text = 'foo "bar" test'
+ self.assertEqual(text, plugin.unquote(*plugin.quote(text)))
+
+ text = '"bar" foo test'
+ self.assertEqual(text, plugin.unquote(*plugin.quote(text)))
+
+ text = 'foo test "bar" '
+ self.assertEqual(text, plugin.unquote(*plugin.quote(text)))
+
+ text = 'foo test "bar"'
+ self.assertEqual(text, plugin.unquote(*plugin.quote(text)))
+
+ text = '"foo bar test'
+ self.assertEqual(text, plugin.unquote(*plugin.quote(text)))
+
+ text = 'foo "bar" test'
+ new_text, replaces = plugin.quote(text)
+ self.assertEqual('foo test', plugin.unquote(new_text, replaces, ''))
+
+ def testChannelQuoting(self):
+ text = '#sugar'
+ text, replaces = plugin.quote(text)
+ self.assertEqual('#foo', plugin.unquote(text, replaces, '#foo'))
+
+ text = '#sugar'
+ text, replaces = plugin.quote(text)
+ self.assertEqual('#sugar', plugin.unquote(text, replaces))
+
+ text = '#sugar test'
+ text, replaces = plugin.quote(text)
+ self.assertEqual('#foo test', plugin.unquote(text, replaces, '#foo'))
+
+ text = 'test #sugar'
+ text, replaces = plugin.quote(text)
+ self.assertEqual('test #foo', plugin.unquote(text, replaces, '#foo'))
+
+ text = 'test #'
+ text, replaces = plugin.quote(text)
+ self.assertEqual('test ?', plugin.unquote(text, replaces, '?'))
+
+ def testNickQuoting(self):
+ text = 'foo:_foo test foobar2 foobar'
+ text, replaces = plugin.quote(text, ['foo', 'test', 'foobar'])
+ self.assertEqual('003:_foo 002 foobar2 001', text)
+ self.assertEqual({'003': 'foo',
+ '002': 'test',
+ '001': 'foobar'}, replaces)
+
+ text = 'foo:foo,foo'
+ text, replaces = plugin.quote(text, ['foo'])
+ self.assertEqual('001:002,003', text)
+ self.assertEqual({'001': 'foo',
+ '002': 'foo',
+ '003': 'foo'}, replaces)
+
+ def testNickLangQuoting(self):
+ text = 'foo-ru:foo-pt-PT foo'
+ text, replaces = plugin.quote(text, ['foo'])
+ self.assertEqual('001:002 003', text)
+ self.assertEqual({'001': 'foo',
+ '002': 'foo',
+ '003': 'foo'}, replaces)
+
+ text = 'foo-test-ru:foo-test-pt-PT'
+ text, replaces = plugin.quote(text, ['foo', 'foo-test'])
+ self.assertEqual('001:002', text)
+ self.assertEqual({'001': 'foo-test',
+ '002': 'foo-test'}, replaces)