From 74024d9d8ff4442f51d7178418e9b041af0809eb Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Tue, 26 Apr 2011 11:25:17 +0000 Subject: Extract from puppet module --- 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] [] [] + + Starts relaying between specified (or current one if 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] [] [|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): + """[] + + 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): + """[] + + 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): + """[] + + 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): + """[] + + 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) -- cgit v0.9.1