mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-11 20:59:42 +01:00
Compare commits
14 Commits
225c249ec2
...
1952dae6f7
Author | SHA1 | Date | |
---|---|---|---|
1952dae6f7 | |||
|
a7216d290f | ||
|
649048443e | ||
|
65ab65cbb1 | ||
|
a7c4c9bd78 | ||
|
936d7ebfea | ||
|
d919e2133d | ||
|
3b25a94b46 | ||
|
2293d1c129 | ||
|
67a39a3adb | ||
|
4b82934131 | ||
|
c8053dad54 | ||
|
69c948bd5f | ||
|
8a52902727 |
@ -1,276 +0,0 @@
|
||||
# Spanish translation for limnoria
|
||||
# Copyright (c) 2015 Limnoria Contributors 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-12-24 15:42+0000\n"
|
||||
"PO-Revision-Date: 2015-01-01 16:35+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-01 16:48+0000\n"
|
||||
"X-Generator: Launchpad (build 17286)\n"
|
||||
|
||||
#: plugin.py:46
|
||||
#, docstring
|
||||
msgid ""
|
||||
"This plugin provides access to administrative commands, such as\n"
|
||||
" adding capabilities, managing ignore lists, and joining channels.\n"
|
||||
" This is a core Supybot plugin that should not be removed!"
|
||||
msgstr ""
|
||||
"Este plugin proporciona acceso a los comandos de administración, tales como\n"
|
||||
"la adición de capacidades, la gestión ignoran listas y canales de unión.\n"
|
||||
"Se trata de un complemento del núcleo Supybot que no se debe quitar!"
|
||||
|
||||
#: plugin.py:57
|
||||
#, docstring
|
||||
msgid "Nick/channel temporarily unavailable."
|
||||
msgstr "Nick/canal disponible temporalmente."
|
||||
|
||||
#: plugin.py:75
|
||||
msgid "Cannot join %s, it's full."
|
||||
msgstr "No puede unirse a %s, está lleno."
|
||||
|
||||
#: plugin.py:83
|
||||
msgid "Cannot join %s, I was not invited."
|
||||
msgstr "No puede unirse a %s, no me invitaron."
|
||||
|
||||
#: plugin.py:91
|
||||
msgid "Cannot join %s, I am banned."
|
||||
msgstr "No pudo unirse a %s, estoy baneado."
|
||||
|
||||
#: plugin.py:99
|
||||
msgid "Cannot join %s, my keyword was wrong."
|
||||
msgstr "No puede unirse a %s, mi palabra estaba mal."
|
||||
|
||||
#: plugin.py:107 plugin.py:116
|
||||
msgid "Cannot join %s, I'm not identified with NickServ."
|
||||
msgstr "No puede unirse a %s, yo no estoy identificado con NickServ."
|
||||
|
||||
#: plugin.py:146
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<channel> [<key>]\n"
|
||||
"\n"
|
||||
" Tell the bot to join the given channel. If <key> is given, it is "
|
||||
"used\n"
|
||||
" when attempting to join the channel.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Canal> [<key>]\n"
|
||||
"\n"
|
||||
"Dígale al bot para unirse al canal dado. Si <key> se da, que se utiliza\n"
|
||||
"al intentar entrar al canal.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:159
|
||||
msgid "I'm already too close to maximum number of channels for this network."
|
||||
msgstr "Ya estoy demasiado cerca de número máximo de canales para esta red."
|
||||
|
||||
#: plugin.py:168
|
||||
#, docstring
|
||||
msgid ""
|
||||
"takes no arguments\n"
|
||||
"\n"
|
||||
" Returns the channels the bot is on. Must be given in private, in "
|
||||
"order\n"
|
||||
" to protect the secrecy of secret channels.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"no tiene argumentos\n"
|
||||
"\n"
|
||||
"Devuelve los canales del bot está encendido. Debe ser dado en privado, con "
|
||||
"el fin\n"
|
||||
"para proteger el secreto de canales secretos.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:178
|
||||
msgid "I'm not currently in any channels."
|
||||
msgstr "No estoy actualmente en ningún canal."
|
||||
|
||||
#: plugin.py:184
|
||||
msgid "My connection is restricted, I can't change nicks."
|
||||
msgstr "Mi conexión es restringida, no puedo cambiar nicks."
|
||||
|
||||
#: plugin.py:191
|
||||
msgid "Someone else is already using that nick."
|
||||
msgstr "Alguien más ya está utilizando ese nick."
|
||||
|
||||
#: plugin.py:198
|
||||
msgid "I can't change nick, I'm currently banned in %s."
|
||||
msgstr "No puedo cambiar de nick, actualmente Estoy prohibido en %s."
|
||||
|
||||
#: plugin.py:206
|
||||
msgid "I can't change nicks, the server said %q."
|
||||
msgstr "No puedo cambiar de nicks, dijo el servidor %q."
|
||||
|
||||
#: plugin.py:220
|
||||
#, docstring
|
||||
msgid ""
|
||||
"[<nick>] [<network>]\n"
|
||||
"\n"
|
||||
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
|
||||
" bot's current nick.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"[<nick>] [<red>]\n"
|
||||
"\n"
|
||||
"Cambia el nick del bot a <nick>. Si no se da nick, devuelve el\n"
|
||||
"actual nick del bot.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:237
|
||||
#, docstring
|
||||
msgid ""
|
||||
"[<channel>] [<reason>]\n"
|
||||
"\n"
|
||||
" Tells the bot to part the list of channels you give it. <channel> "
|
||||
"is\n"
|
||||
" only necessary if you want the bot to part a channel other than the\n"
|
||||
" current channel. If <reason> is specified, use it as the part\n"
|
||||
" message.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"[<canal>] [<razón>]\n"
|
||||
"\n"
|
||||
"Le dice al bot a parte de la lista de canales que le des. <canal> es\n"
|
||||
"sólo es necesario si desea que el bot a desprenderse de un canal que no sea "
|
||||
"el\n"
|
||||
"canal actual. Si <razón> se especifica, lo utilizan como parte\n"
|
||||
"mensaje.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:255
|
||||
msgid "I'm not in %s."
|
||||
msgstr "No estoy en %s."
|
||||
|
||||
#: plugin.py:267
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<name|hostmask> <capability>\n"
|
||||
"\n"
|
||||
" Gives the user specified by <name> (or the user to whom "
|
||||
"<hostmask>\n"
|
||||
" currently maps) the specified capability <capability>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<nombre | hostmask> <capacidad>\n"
|
||||
"\n"
|
||||
"Le da al usuario especificado por <nombre> (o el usuario a quien <hostmask>\n"
|
||||
"Actualmente los mapas) la capacidad especificada <capacidad>\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:287
|
||||
msgid ""
|
||||
"The \"owner\" capability can't be added in the bot. Use the supybot-adduser "
|
||||
"program (or edit the users.conf file yourself) to add an owner capability."
|
||||
msgstr ""
|
||||
"La capacidad de \"propietario\" no se puede añadir en el bot. Utilice el "
|
||||
"programa de usuario Supybot-add (o editar el users.conf archivo usted mismo) "
|
||||
"para añadir una capacidad de propietario."
|
||||
|
||||
#: plugin.py:298
|
||||
msgid "You can't add capabilities you don't have."
|
||||
msgstr "No se puede agregar capacidades que usted no tiene."
|
||||
|
||||
#: plugin.py:303
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<name|hostmask> <capability>\n"
|
||||
"\n"
|
||||
" Takes from the user specified by <name> (or the user to whom\n"
|
||||
" <hostmask> currently maps) the specified capability "
|
||||
"<capability>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Nombre | hostmask> <capacidad>\n"
|
||||
"\n"
|
||||
"Toma del usuario especificado por <nombre> (o el usuario a quien\n"
|
||||
"<Hostmask> Actualmente los mapas) la capacidad especificada <capacidad>\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:315
|
||||
msgid "That user doesn't have that capability."
|
||||
msgstr "Ese usuario no tiene esa capacidad."
|
||||
|
||||
#: plugin.py:317
|
||||
msgid "You can't remove capabilities you don't have."
|
||||
msgstr "No se puede quitar capacidades que usted no tiene."
|
||||
|
||||
#: plugin.py:325
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<hostmask|nick> [<expires>]\n"
|
||||
"\n"
|
||||
" This will set a persistent ignore on <hostmask> or the hostmask\n"
|
||||
" currently associated with <nick>. <expires> is an optional "
|
||||
"argument\n"
|
||||
" specifying when (in \"seconds from now\") the ignore will "
|
||||
"expire; if\n"
|
||||
" it isn't given, the ignore will never automatically expire.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Hostmask | nick> [<expira>]\n"
|
||||
"\n"
|
||||
"Esto establecerá una persistente ignoran en <hostmask> o la hostmask\n"
|
||||
"actualmente asociados con <nick>. <Expira> es un argumento opcional\n"
|
||||
"especificar cuándo (en \"segundo a partir de ahora\") expirará el ignorar; "
|
||||
"si\n"
|
||||
"no se le da, el ignorar nunca expirará automáticamente.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:338
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<hostmask|nick>\n"
|
||||
"\n"
|
||||
" This will remove the persistent ignore on <hostmask> or the\n"
|
||||
" hostmask currently associated with <nick>.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Hostmask | nick>\n"
|
||||
"\n"
|
||||
"Esto eliminará la persistente ignoran en <hostmask> o la\n"
|
||||
"hostmask actualmente asociada con <nick>.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:347
|
||||
msgid "%s wasn't in the ignores database."
|
||||
msgstr "%S no estaba en la base de datos ignorados."
|
||||
|
||||
#: plugin.py:352
|
||||
#, docstring
|
||||
msgid ""
|
||||
"takes no arguments\n"
|
||||
"\n"
|
||||
" Lists the hostmasks that the bot is ignoring.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"no tiene argumentos\n"
|
||||
"\n"
|
||||
"Enumera los sus hosts que el bot está ignorando.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:360
|
||||
msgid "I'm not currently globally ignoring anyone."
|
||||
msgstr "No estoy actualmente haciendo caso omiso a nivel mundial a nadie."
|
||||
|
||||
#: plugin.py:364
|
||||
#, docstring
|
||||
msgid ""
|
||||
"takes no arguments\n"
|
||||
"\n"
|
||||
" Clears the current send queue for this network.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"no tiene argumentos\n"
|
||||
"\n"
|
||||
"Borra la cola de envío actual de esta red.\n"
|
||||
" "
|
@ -1,151 +0,0 @@
|
||||
# Spanish translation for limnoria
|
||||
# Copyright (c) 2015 Limnoria contributors 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-12-24 15:42+0000\n"
|
||||
"PO-Revision-Date: 2015-01-01 16:43+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-01 17:00+0000\n"
|
||||
"X-Generator: Launchpad (build 17286)\n"
|
||||
|
||||
#: plugin.py:46
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Returns the channel the msg came over or the channel given in args.\n"
|
||||
"\n"
|
||||
" If the channel was given in args, args is modified (the channel is\n"
|
||||
" removed).\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Devuelve el msg del canal vino o el canal dado en args.\n"
|
||||
"\n"
|
||||
"Si el canal se le dio en args, args es modificado (el canal está\n"
|
||||
"eliminado).\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:105
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Encodes [a-z0-9.]+ into [a-z][a-z0-9].\n"
|
||||
" Format: a<number of escaped chars>a(<index>d)+<word without dots>."
|
||||
msgstr ""
|
||||
"Codifica [a-z0-9.] + En [az] [a-z0-9].\n"
|
||||
"Formato: un <número de caracteres escapados> a (<índice> d) + <palabra sin "
|
||||
"puntos>."
|
||||
|
||||
#: plugin.py:221
|
||||
msgid " at least"
|
||||
msgstr " al menos"
|
||||
|
||||
#: plugin.py:223 plugin.py:228
|
||||
msgid ""
|
||||
"<an alias,%s %n>\n"
|
||||
"\n"
|
||||
"Alias for %q."
|
||||
msgstr ""
|
||||
"<Un alias,%s %n>\n"
|
||||
"\n"
|
||||
"Alias para %q."
|
||||
|
||||
#: plugin.py:224 plugin.py:229
|
||||
msgid "argument"
|
||||
msgstr "argumento"
|
||||
|
||||
#: plugin.py:234
|
||||
#, docstring
|
||||
msgid ""
|
||||
"This plugin allows users to define aliases to commands and combinations\n"
|
||||
" of commands (via nesting)."
|
||||
msgstr ""
|
||||
"Este plugin permite a los usuarios definir los alias de comandos y "
|
||||
"combinaciones\n"
|
||||
"de comandos (a través de anidación)."
|
||||
|
||||
#: plugin.py:299
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<alias>\n"
|
||||
"\n"
|
||||
" Locks an alias so that no one else can change it.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Alias>\n"
|
||||
"\n"
|
||||
"Bloquea un alias para que nadie más puede cambiarlo.\n"
|
||||
"\t\tHay saltos de línea aquí. Cada uno\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:308 plugin.py:322
|
||||
msgid "There is no such alias."
|
||||
msgstr "No hay tal alias."
|
||||
|
||||
#: plugin.py:313
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<alias>\n"
|
||||
"\n"
|
||||
" Unlocks an alias so that people can define new aliases over it.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Alias>\n"
|
||||
"\n"
|
||||
"Desbloquea un alias para que las personas puedan definir nuevos alias sobre "
|
||||
"él.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:334
|
||||
msgid "That name isn't valid. Try %q instead."
|
||||
msgstr "Ese nombre no es válido. Trate% q en vez."
|
||||
|
||||
#: plugin.py:379
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<name> <command>\n"
|
||||
"\n"
|
||||
" Defines an alias <name> that executes <command>. The <command>\n"
|
||||
" should be in the standard \"command argument [nestedcommand "
|
||||
"argument]\"\n"
|
||||
" arguments to the alias; they'll be filled with the first, second, "
|
||||
"etc.\n"
|
||||
" arguments. $1, $2, etc. can be used for required arguments. @1, "
|
||||
"@2,\n"
|
||||
" etc. can be used for optional arguments. $* simply means \"all\n"
|
||||
" remaining arguments,\" and cannot be combined with optional "
|
||||
"arguments.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Nombre> <comando>\n"
|
||||
"\n"
|
||||
"Define un alias <nombre> que ejecuta <command>. El <comando>\n"
|
||||
"debe estar en el estándar \"argumento de comando [argumento "
|
||||
"nestedcommand]\"\n"
|
||||
"argumentos al alias; que van a ser llenados con el primero, segundo, etc.\n"
|
||||
"argumentos. $ 1, $ 2, etc. puede ser usado para los argumentos necesarios. @ "
|
||||
"1, @ 2,\n"
|
||||
"etc. puede ser utilizado para argumentos opcionales. $ * Simplemente "
|
||||
"significa \"todo\n"
|
||||
"restantes argumentos, \"y no puede ser combinado con argumentos opcionales.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:402
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<name>\n"
|
||||
"\n"
|
||||
" Removes the given alias, if unlocked.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Nombre>\n"
|
||||
"\n"
|
||||
"Elimina los alias dados, si desbloqueado.\n"
|
||||
" "
|
@ -1,156 +0,0 @@
|
||||
# Spanish translation for limnoria
|
||||
# Copyright (c) 2015 Limnoria contributors 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-12-24 15:42+0000\n"
|
||||
"PO-Revision-Date: 2015-01-01 16:56+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-01 17:04+0000\n"
|
||||
"X-Generator: Launchpad (build 17286)\n"
|
||||
|
||||
#: config.py:49
|
||||
msgid ""
|
||||
"Determines whether\n"
|
||||
" the bot should require people trying to use this plugin to be in the\n"
|
||||
" channel they wish to anonymously send to."
|
||||
msgstr ""
|
||||
"Determina si\n"
|
||||
"el robot debe requerir la gente tratando de utilizar este plugin para poder "
|
||||
"estar en el\n"
|
||||
"canal que desean enviar anónimamente."
|
||||
|
||||
#: config.py:53
|
||||
msgid ""
|
||||
"Determines whether the bot should require\n"
|
||||
" people trying to use this plugin to be registered."
|
||||
msgstr ""
|
||||
"Determina si el bot debe exigir\n"
|
||||
"personas que tratan de utilizar este plugin para ser registrados."
|
||||
|
||||
#: config.py:56
|
||||
msgid ""
|
||||
"Determines what capability (if any) the bot should\n"
|
||||
" require people trying to use this plugin to have."
|
||||
msgstr ""
|
||||
"Determina lo que la capacidad (si lo hay) que el bot debe\n"
|
||||
"requieren que las personas tratando de usar este plugin para poder tener."
|
||||
|
||||
#: config.py:59
|
||||
msgid ""
|
||||
"Determines whether the bot will allow the\n"
|
||||
" \"tell\" command to be used. If true, the bot will allow the \"tell\"\n"
|
||||
" command to send private messages to other users."
|
||||
msgstr ""
|
||||
"Determina si el bot permitirá al\n"
|
||||
"Comando \"decirle\" a utilizar. De ser cierto, el bot le permitirá al "
|
||||
"\"decir\"\n"
|
||||
"comando para enviar mensajes privados a otros usuarios."
|
||||
|
||||
#: plugin.py:41
|
||||
#, docstring
|
||||
msgid ""
|
||||
"This plugin allows users to act through the bot anonymously. The 'do'\n"
|
||||
" command has the bot perform an anonymous action in a given channel, and\n"
|
||||
" the 'say' command allows other people to speak through the bot. Since\n"
|
||||
" this can be fairly well abused, you might want to set\n"
|
||||
" supybot.plugins.Anonymous.requireCapability so only users with that\n"
|
||||
" capability can use this plugin. For extra security, you can require "
|
||||
"that\n"
|
||||
" the user be *in* the channel they are trying to address anonymously "
|
||||
"with\n"
|
||||
" supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n"
|
||||
" that the user be registered by setting\n"
|
||||
" supybot.plugins.Anonymous.requireRegistration.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Este plugin permite a los usuarios actúan a través de la bot anónima. El "
|
||||
"\"hacer\"\n"
|
||||
"comando tiene el robot realice una acción anónimo en un canal determinado, "
|
||||
"y\n"
|
||||
"el 'dicen' comando permite que otras personas hablan a través de la bot. "
|
||||
"Desde\n"
|
||||
"esto puede ser bastante maltratado, es posible que desee establecer\n"
|
||||
"supybot.plugins.Anonymous.requireCapability tan sólo los usuarios con que\n"
|
||||
"capacidad puede utilizar este plugin. Para mayor seguridad, se puede "
|
||||
"requerir que\n"
|
||||
"el usuario ser * en * el canal que están tratando de hacer frente de forma "
|
||||
"anónima con\n"
|
||||
"supybot.plugins.Anonymous.requirePresenceInChannel, o puede requerir\n"
|
||||
"que el usuario se ha registrado mediante el establecimiento de\n"
|
||||
"supybot.plugins.Anonymous.requireRegistration.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:65
|
||||
msgid "You must be in %s to %q in there."
|
||||
msgstr "Usted debe estar en% s para% q allí."
|
||||
|
||||
#: plugin.py:69
|
||||
msgid "I'm lobotomized in %s."
|
||||
msgstr "Estoy lobotomizado en%s."
|
||||
|
||||
#: plugin.py:72
|
||||
msgid ""
|
||||
"That channel has set its capabilities so as to disallow the use of this "
|
||||
"plugin."
|
||||
msgstr ""
|
||||
"Ese canal ha puesto sus capacidades a fin de no permitir el uso de este "
|
||||
"plugin."
|
||||
|
||||
#: plugin.py:75
|
||||
msgid ""
|
||||
"This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is "
|
||||
"False)."
|
||||
msgstr ""
|
||||
"Este comando está desactivado (supybot.plugins.Anonymous.allowPrivateTarget "
|
||||
"es falso)."
|
||||
|
||||
#: plugin.py:80
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<channel> <text>\n"
|
||||
"\n"
|
||||
" Sends <text> to <channel>.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Canal> <texto>\n"
|
||||
"\n"
|
||||
"Envía <texto> al <canal>.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:92
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<nick> <text>\n"
|
||||
"\n"
|
||||
" Sends <text> to <nick>. Can only be used if\n"
|
||||
" supybot.plugins.Anonymous.allowPrivateTarget is True.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Nick> <texto>\n"
|
||||
"\n"
|
||||
"Envía <texto> a <nick>. Sólo puede utilizarse si\n"
|
||||
"supybot.plugins.Anonymous.allowPrivateTarget es True.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:106
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<channel> <action>\n"
|
||||
"\n"
|
||||
" Performs <action> in <channel>.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Canal> <acción>\n"
|
||||
"\n"
|
||||
"Realiza <action> en <channel>.\n"
|
||||
" "
|
@ -1,137 +0,0 @@
|
||||
# Spanish translation for Limnoria
|
||||
# Copyright (c) 2015 Limnoria 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-12-24 15:43+0000\n"
|
||||
"PO-Revision-Date: 2015-01-02 18:47+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-02 18:48+0000\n"
|
||||
"X-Generator: Launchpad (build 17286)\n"
|
||||
|
||||
#: config.py:46
|
||||
msgid ""
|
||||
"Determines whether this plugin is enabled.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Determina si este plugin está habilitado.\n"
|
||||
" "
|
||||
|
||||
#: config.py:49
|
||||
msgid ""
|
||||
"Determines whether this plugin will automode\n"
|
||||
" owners even if they don't have op/halfop/voice/whatever capability."
|
||||
msgstr ""
|
||||
"Determina si este plugin AutoMode\n"
|
||||
"propietarios incluso si no tienen lo que sea la capacidad op/halfop/voz."
|
||||
|
||||
#: config.py:52
|
||||
msgid ""
|
||||
"Determines whether the bot will\n"
|
||||
" check for 'alternative capabilities' (ie. autoop, autohalfop,\n"
|
||||
" autovoice) in addition to/instead of classic ones."
|
||||
msgstr ""
|
||||
"Determina si el bot\n"
|
||||
"comprobar 'capacidades alternativas \"(es decir. autoop, autohalfop,\n"
|
||||
"autovoz) además de/en lugar de los clásicos."
|
||||
|
||||
#: config.py:56
|
||||
msgid ""
|
||||
"Determines whether the bot will \"fall\n"
|
||||
" through\" to halfop/voicing when auto-opping is turned off but\n"
|
||||
" auto-halfopping/voicing are turned on."
|
||||
msgstr ""
|
||||
"Determina si el bot \"caerá\n"
|
||||
"a través \"de HALFOP / sonoridad cuando auto-Op está apagado pero\n"
|
||||
"auto-halfop a si mismos / sonido están activados."
|
||||
|
||||
#: config.py:60
|
||||
msgid ""
|
||||
"Determines whether the bot will automatically\n"
|
||||
" op people with the <channel>,op capability when they join the channel.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Determina si el bot automáticamente\n"
|
||||
"dar op con el <channel>, capacidad de op cuando se unen al canal.\n"
|
||||
" "
|
||||
|
||||
#: config.py:64
|
||||
msgid ""
|
||||
"Determines whether the bot will automatically\n"
|
||||
" halfop people with the <channel>,halfop capability when they join the\n"
|
||||
" channel."
|
||||
msgstr ""
|
||||
"Determina si el bot automáticamente\n"
|
||||
"personas HALFOP con el <channel>, capacidad halfop cuando se unen a la\n"
|
||||
"canal."
|
||||
|
||||
#: config.py:68
|
||||
msgid ""
|
||||
"Determines whether the bot will automatically\n"
|
||||
" voice people with the <channel>,voice capability when they join the\n"
|
||||
" channel."
|
||||
msgstr ""
|
||||
"Determina si el bot automáticamente\n"
|
||||
"gente de voz con el <channel>, capacidad de voz cuando se unen a la\n"
|
||||
"canal."
|
||||
|
||||
#: config.py:72
|
||||
msgid ""
|
||||
"Determines whether the bot will automatically\n"
|
||||
" ban people who join the channel and are on the banlist."
|
||||
msgstr ""
|
||||
"Determina si el bot automáticamente\n"
|
||||
"prohibir a la gente que se unen al canal y están en la lista de ban."
|
||||
|
||||
#: config.py:75
|
||||
msgid ""
|
||||
"Determines how many seconds the bot\n"
|
||||
" will automatically ban a person when banning."
|
||||
msgstr ""
|
||||
"Determina cuántos segundos el bot\n"
|
||||
"prohibirá automáticamente a una persona cuando la prohibición."
|
||||
|
||||
#: config.py:79
|
||||
msgid ""
|
||||
"Determines how many seconds the bot will wait\n"
|
||||
" before applying a mode. Has no effect on bans."
|
||||
msgstr ""
|
||||
"Determina cuántos segundos esperará el bot\n"
|
||||
"antes de aplicar un modo. No tiene ningún efecto sobre las prohibiciones."
|
||||
|
||||
#: config.py:83
|
||||
msgid ""
|
||||
"Extra modes that will be\n"
|
||||
" applied to a user. Example syntax: user1+o-v user2+v user3-v"
|
||||
msgstr ""
|
||||
"Modos adicionales que serán\n"
|
||||
" se aplica a un usuario. Ejemplo de sintaxis: usuario1 + o-v + v usuario2 "
|
||||
"usuario3-v"
|
||||
|
||||
#: plugin.py:48
|
||||
#, docstring
|
||||
msgid ""
|
||||
"This plugin, when configured, allows the bot to automatically set modes\n"
|
||||
" on users when they join."
|
||||
msgstr ""
|
||||
"Este plugin, cuando se configura, permite al bot para establecer "
|
||||
"automáticamente los modos\n"
|
||||
"en los usuarios cuando se unen."
|
||||
|
||||
#: plugin.py:80
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Determines whether or not a mode has already\n"
|
||||
" been applied."
|
||||
msgstr ""
|
||||
"Determina si un modo ya tiene o \n"
|
||||
"ha aplicado."
|
@ -121,8 +121,9 @@ class Autocomplete(callbacks.Plugin):
|
||||
"""Provides command completion for IRC clients that support it."""
|
||||
|
||||
def _enabled(self, irc, msg):
|
||||
return conf.supybot.protocols.irc.experimentalExtensions() and self.registryValue(
|
||||
"enabled", msg.channel, irc.network
|
||||
return (
|
||||
conf.supybot.protocols.irc.experimentalExtensions()
|
||||
and self.registryValue("enabled", msg.channel, irc.network)
|
||||
)
|
||||
|
||||
def doTagmsg(self, irc, msg):
|
||||
|
@ -1,199 +0,0 @@
|
||||
# Spanish translation for Limnoria
|
||||
# Copyright (c) 2015 Limnoria Contributors 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-03-26 09:31+0000\n"
|
||||
"PO-Revision-Date: 2015-01-02 19:15+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-09 18:01+0000\n"
|
||||
"X-Generator: Launchpad (build 17298)\n"
|
||||
|
||||
#: config.py:42
|
||||
msgid "Would you like to add some bad words?"
|
||||
msgstr "¿Te gustaría añadir alguna mala palabra?"
|
||||
|
||||
#: config.py:43
|
||||
msgid "What words? (separate individual words by spaces)"
|
||||
msgstr "¿Qué palabras? (palabras individuales separadas por espacios)"
|
||||
|
||||
#: config.py:55
|
||||
msgid ""
|
||||
"Determines what words are\n"
|
||||
" considered to be 'bad' so the bot won't say them."
|
||||
msgstr ""
|
||||
"Determina qué palabras son\n"
|
||||
"considera que es \"malo\" por lo que el bot no decirlas."
|
||||
|
||||
#: config.py:58
|
||||
msgid ""
|
||||
"Determines whether the bot will require bad\n"
|
||||
" words to be independent words, or whether it will censor them within "
|
||||
"other\n"
|
||||
" words. For instance, if 'darn' is a bad word, then if this is true, "
|
||||
"'darn'\n"
|
||||
" will be censored, but 'darnit' will not. You probably want this to be\n"
|
||||
" false. After changing this setting, the BadWords regexp needs to be\n"
|
||||
" regenerated by adding/removing a word to the list, or reloading the\n"
|
||||
" plugin."
|
||||
msgstr ""
|
||||
"Determina si el bot requerirá mal\n"
|
||||
"palabras sean palabras independientes, o si se van a censurar dentro de "
|
||||
"otra\n"
|
||||
"palabras. Por ejemplo, si 'maldito' es una mala palabra, entonces si esto es "
|
||||
"cierto, 'maldito'\n"
|
||||
"serán censurados, pero 'darnit' no lo hará. Es probable que esta sea\n"
|
||||
"falsa. Después de cambiar este ajuste, los badwords expresión regular tiene "
|
||||
"que ser\n"
|
||||
"regenerado por añadir/eliminar una palabra a la lista, o volver a cargar el\n"
|
||||
"plugin."
|
||||
|
||||
#: config.py:75
|
||||
msgid ""
|
||||
"Determines what characters will replace bad words; a\n"
|
||||
" chunk of these characters matching the size of the replaced bad word "
|
||||
"will\n"
|
||||
" be used to replace the bad words you've configured."
|
||||
msgstr ""
|
||||
"Determina qué caracteres reemplazarán malas palabras; un\n"
|
||||
"parte de estos caracteres que coincida con el tamaño de la mala palabra "
|
||||
"reemplazado voluntad\n"
|
||||
"usarse para reemplazar las malas palabras que ha configurado."
|
||||
|
||||
#: config.py:83
|
||||
msgid ""
|
||||
"Determines the manner in which\n"
|
||||
" bad words will be replaced. 'nastyCharacters' (the default) will "
|
||||
"replace a\n"
|
||||
" bad word with the same number of 'nasty characters' (like those used in\n"
|
||||
" comic books; configurable by supybot.plugins.BadWords.nastyChars).\n"
|
||||
" 'simple' will replace a bad word with a simple strings (regardless of "
|
||||
"the\n"
|
||||
" length of the bad word); this string is configurable via\n"
|
||||
" supybot.plugins.BadWords.simpleReplacement."
|
||||
msgstr ""
|
||||
"Determina la manera en que\n"
|
||||
"serán reemplazados malas palabras. 'NastyCharacters' (por defecto), "
|
||||
"sustituirá a un\n"
|
||||
"mala palabra con el mismo número de 'personajes desagradables' (como los "
|
||||
"utilizados en\n"
|
||||
"cómics; configurable por supybot.plugins.BadWords.nastyChars).\n"
|
||||
"\"Simple\" reemplazará una mala palabra con un simple cuerdas "
|
||||
"(independientemente de la\n"
|
||||
"longitud de la mala palabra); esta cadena es configurable a través de\n"
|
||||
"supybot.plugins.BadWords.simpleReplacement."
|
||||
|
||||
#: config.py:91
|
||||
msgid ""
|
||||
"Determines what word will replace bad\n"
|
||||
" words if the replacement method is 'simple'."
|
||||
msgstr ""
|
||||
"Determina qué palabra reemplazará mal\n"
|
||||
"es decir, si el método de sustitución es \"simple\"."
|
||||
|
||||
#: config.py:94
|
||||
msgid ""
|
||||
"Determines whether the bot will strip\n"
|
||||
" formatting characters from messages before it checks them for bad "
|
||||
"words.\n"
|
||||
" If this is False, it will be relatively trivial to circumvent this "
|
||||
"plugin's\n"
|
||||
" filtering. If it's True, however, it will interact poorly with other\n"
|
||||
" plugins that do coloring or bolding of text."
|
||||
msgstr ""
|
||||
"Determina si el bot se tira\n"
|
||||
"caracteres de formato de los mensajes antes de que se les comprueba malas "
|
||||
"palabras.\n"
|
||||
"Si esto es falso, será relativamente trivial para eludir de este plugin\n"
|
||||
"filtrado. Si es cierto, sin embargo, va a interactuar mal con otro\n"
|
||||
"plugins que hacen coloración o la negrita del texto."
|
||||
|
||||
#: config.py:101
|
||||
msgid ""
|
||||
"Determines whether the bot will kick people with\n"
|
||||
" a warning when they use bad words."
|
||||
msgstr ""
|
||||
"Determina si el bot se iniciará a las personas con\n"
|
||||
"una advertencia cuando use malas palabras."
|
||||
|
||||
#: config.py:104
|
||||
msgid ""
|
||||
"You have been kicked for using a word\n"
|
||||
" prohibited in the presence of this bot. Please use more appropriate\n"
|
||||
" language in the future."
|
||||
msgstr ""
|
||||
"Has sido expulsado por usar una palabra\n"
|
||||
"prohibido en la presencia de este robot. Por favor, use más apropiado\n"
|
||||
"idioma en el futuro."
|
||||
|
||||
#: config.py:106
|
||||
msgid ""
|
||||
"Determines the kick message used by the\n"
|
||||
" bot when kicking users for saying bad words."
|
||||
msgstr ""
|
||||
"Determina el mensaje patada utilizado por el\n"
|
||||
"bot cuando patadas usuarios por decir malas palabras."
|
||||
|
||||
#: plugin.py:46
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Maintains a list of words that the bot is not allowed to say.\n"
|
||||
" Can also be used to kick people that say these words, if the bot\n"
|
||||
" has op."
|
||||
msgstr ""
|
||||
"Mantiene una lista de palabras que el bot no está permitido decir.\n"
|
||||
"También se puede utilizar para echar a la gente que dicen estas palabras, si "
|
||||
"el bot\n"
|
||||
"tiene op."
|
||||
|
||||
#: plugin.py:115
|
||||
#, docstring
|
||||
msgid ""
|
||||
"takes no arguments\n"
|
||||
"\n"
|
||||
" Returns the list of words being censored.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"no tiene argumentos\n"
|
||||
"\n"
|
||||
"Devuelve la lista de palabras que se censuró.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:125
|
||||
msgid "I'm not currently censoring any bad words."
|
||||
msgstr "No estoy actualmente censurando las malas palabras."
|
||||
|
||||
#: plugin.py:130
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<word> [<word> ...]\n"
|
||||
"\n"
|
||||
" Adds all <word>s to the list of words being censored.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Palabra> [<palabra> ...]\n"
|
||||
"\n"
|
||||
"Añade todos <palabra> s a la lista de palabras que se censuró.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:142
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<word> [<word> ...]\n"
|
||||
"\n"
|
||||
" Removes <word>s from the list of words being censored.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Palabra> [<palabra> ...]\n"
|
||||
"\n"
|
||||
"Elimina <palabra> s de la lista de palabras que se censuró.\n"
|
||||
" "
|
@ -18,6 +18,8 @@ Basically, it replaces the standard 'Error: <x> is not a valid command.'
|
||||
messages with messages kept in a database, able to give more personable
|
||||
responses.
|
||||
|
||||
``$command`` in the message will be replaced by the command's name.
|
||||
|
||||
.. _commands-Dunno:
|
||||
|
||||
Commands
|
||||
|
@ -1,288 +0,0 @@
|
||||
# Spanish translation for Limnoria
|
||||
# Copyright (c) 2015 Limnoria 2015
|
||||
# This file is distributed under the same license as the Limnoria package.
|
||||
# Aaron Farias <timido@ubuntu.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: limnoria\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2014-12-24 15:42+0000\n"
|
||||
"PO-Revision-Date: 2015-01-01 18:06+0000\n"
|
||||
"Last-Translator: Aaron Farias <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-01-01 18:08+0000\n"
|
||||
"X-Generator: Launchpad (build 17286)\n"
|
||||
|
||||
#: config.py:39
|
||||
msgid ""
|
||||
"The Google plugin has the functionality to watch for URLs\n"
|
||||
" that match a specific pattern. (We call this a snarfer)\n"
|
||||
" When supybot sees such a URL, it will parse the web page\n"
|
||||
" for information and reply with the results."
|
||||
msgstr ""
|
||||
"El plugin de Google tiene la funcionalidad de ver para las direcciones URL\n"
|
||||
"que coinciden con un patrón específico. (A esto le llamamos una Snarfer)\n"
|
||||
"Cuando Supybot ve un enlace, será analizar la página web\n"
|
||||
"de información y respuesta con los resultados."
|
||||
|
||||
#: config.py:43
|
||||
msgid "Do you want the Google search snarfer enabled by default?"
|
||||
msgstr "¿Quieres que la búsqueda Snarfer Google activado por defecto?"
|
||||
|
||||
#: config.py:89
|
||||
#, docstring
|
||||
msgid "Value must be 1 <= n <= 8"
|
||||
msgstr "El valor debe ser 1 <= n <= 8"
|
||||
|
||||
#: config.py:100
|
||||
msgid ""
|
||||
"Determines the URL that will be sent to Google for\n"
|
||||
" the Referer field of the search requests. If this value is empty, a\n"
|
||||
" Referer will be generated in the following format:\n"
|
||||
" http://$server/$botName"
|
||||
msgstr ""
|
||||
"Determina la dirección URL que se enviará a Google para\n"
|
||||
"el campo de referencia de las solicitudes de búsqueda. Si este valor está "
|
||||
"vacío, una\n"
|
||||
"Árbitro será generado en el siguiente formato:\n"
|
||||
"http: // servidor $ / $ botName"
|
||||
|
||||
#: config.py:105
|
||||
msgid ""
|
||||
"Determines the base URL used for\n"
|
||||
" requests."
|
||||
msgstr ""
|
||||
"Determina la URL base que se utiliza para\n"
|
||||
"peticiones."
|
||||
|
||||
#: config.py:108
|
||||
msgid ""
|
||||
"Determines whether the search snarfer is\n"
|
||||
" enabled. If so, messages (even unaddressed ones) beginning with the "
|
||||
"word\n"
|
||||
" 'google' will result in the first URL Google returns being sent to the\n"
|
||||
" channel."
|
||||
msgstr ""
|
||||
"Determina si la búsqueda es Snarfer\n"
|
||||
"habilitado. Si es así, los mensajes (incluso sin dirección) comienza con la "
|
||||
"palabra\n"
|
||||
"'Google' dará lugar a la primera URL Google devuelve está enviando al\n"
|
||||
"canal."
|
||||
|
||||
#: config.py:113
|
||||
msgid ""
|
||||
"Determines whether the word 'google' in the\n"
|
||||
" bot's output will be made colorful (like Google's logo)."
|
||||
msgstr ""
|
||||
"Determina si la palabra \"Google\" en el\n"
|
||||
"salida del bot se hará colorido (como el logotipo de Google)."
|
||||
|
||||
#: config.py:116
|
||||
msgid "Determines whether results are bolded."
|
||||
msgstr "Determina si los resultados están en negrita."
|
||||
|
||||
#: config.py:118
|
||||
msgid ""
|
||||
"Determines whether results are sent in\n"
|
||||
" different lines or all in the same one."
|
||||
msgstr ""
|
||||
"Determina si los resultados se envían en\n"
|
||||
"diferentes líneas o todos en el mismo."
|
||||
|
||||
#: config.py:121
|
||||
msgid ""
|
||||
"Determines the maximum number of results returned\n"
|
||||
" from the google command."
|
||||
msgstr ""
|
||||
"Determina el número máximo de resultados devueltos\n"
|
||||
"desde el comando google."
|
||||
|
||||
#: config.py:124
|
||||
msgid ""
|
||||
"Determines what default language is used in\n"
|
||||
" searches. If left empty, no specific language will be requested."
|
||||
msgstr ""
|
||||
"Determina el idioma por defecto se utiliza en\n"
|
||||
"búsquedas. Si se deja vacío, se solicitará ningún lenguaje específico."
|
||||
|
||||
#: config.py:124
|
||||
msgid "en"
|
||||
msgstr "en"
|
||||
|
||||
#: config.py:127
|
||||
msgid ""
|
||||
"Determines what level of search filtering to use\n"
|
||||
" by default. 'active' - most filtering, 'moderate' - default filtering,\n"
|
||||
" 'off' - no filtering"
|
||||
msgstr ""
|
||||
"Determina el nivel de filtrado de búsqueda qué usar\n"
|
||||
"de forma predeterminada. \"Activo\" - más filtrado, \"moderado\" - Filtrado "
|
||||
"por defecto,\n"
|
||||
"\"Off\" - sin filtrado"
|
||||
|
||||
#: plugin.py:52
|
||||
#, docstring
|
||||
msgid ""
|
||||
"This is a simple plugin to provide access to the Google services we\n"
|
||||
" all know and love from our favorite IRC bot."
|
||||
msgstr ""
|
||||
"Este es un sencillo plugin para proporcionar acceso a los servicios que "
|
||||
"Google\n"
|
||||
"todos conocemos y amamos de nuestro bot de IRC favorito."
|
||||
|
||||
#: plugin.py:86
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Perform a search using Google's AJAX API.\n"
|
||||
" search(\"search phrase\", options={})\n"
|
||||
"\n"
|
||||
" Valid options are:\n"
|
||||
" smallsearch - True/False (Default: False)\n"
|
||||
" filter - {active,moderate,off} (Default: \"moderate\")\n"
|
||||
" language - Restrict search to documents in the given language\n"
|
||||
" (Default: \"lang_en\")\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"Realice una búsqueda usando API AJAX de Google.\n"
|
||||
"Búsqueda (\"frase de búsqueda\", las opciones = {})\n"
|
||||
"\n"
|
||||
"Las opciones válidas son:\n"
|
||||
"smallsearch - True / False (predeterminado: False)\n"
|
||||
"Filtro - {activa, moderada off} (por defecto: \"moderado\")\n"
|
||||
"idioma - Restringir la búsqueda a los documentos en el idioma que tenga\n"
|
||||
"(Por defecto: \"lang_en\")\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:125 plugin.py:191
|
||||
msgid "We broke The Google!"
|
||||
msgstr "Rompimos El Google!"
|
||||
|
||||
#: plugin.py:150
|
||||
msgid "No matches found."
|
||||
msgstr "No se encontró ninguna coincidencia."
|
||||
|
||||
#: plugin.py:158
|
||||
#, docstring
|
||||
msgid ""
|
||||
"[--snippet] <search>\n"
|
||||
"\n"
|
||||
" Does a google search, but only returns the first result.\n"
|
||||
" If option --snippet is given, returns also the page text snippet.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"[--snippet] <Búsqueda>\n"
|
||||
"\n"
|
||||
"¿Tiene una búsqueda en Google, pero sólo devuelve el primer resultado.\n"
|
||||
"Si se da la opción --snippet, devuelve también el fragmento de texto de la "
|
||||
"página.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:175
|
||||
msgid "Google found nothing."
|
||||
msgstr "Google no encontró nada."
|
||||
|
||||
#: plugin.py:180
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<search> [--{filter,language} <value>]\n"
|
||||
"\n"
|
||||
" Searches google.com for the given string. As many results as can "
|
||||
"fit\n"
|
||||
" are included. --language accepts a language abbreviation; --filter\n"
|
||||
" accepts a filtering level ('active', 'moderate', 'off').\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Búsqueda> [- {filtro, idioma} <valor>]\n"
|
||||
"\n"
|
||||
"Busca en google.com para la cadena dada. Como muchos resultados como la "
|
||||
"capacidad es\n"
|
||||
"están incluidos. --language acepta una abreviatura del idioma; --filter\n"
|
||||
"acepta un nivel de filtrado (\"activa\", \"moderado\", \"off\").\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:208
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<url>\n"
|
||||
"\n"
|
||||
" Returns a link to the cached version of <url> if it is available.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<url>\n"
|
||||
"\n"
|
||||
"Devuelve un enlace a la versión en caché de <url> si está disponible.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:219
|
||||
msgid "Google seems to have no cache for that site."
|
||||
msgstr "Google parece no tener memoria caché para ese sitio."
|
||||
|
||||
#: plugin.py:224
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<search string> <search string> [<search string> ...]\n"
|
||||
"\n"
|
||||
" Returns the results of each search, in order, from greatest number\n"
|
||||
" of results to least.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<Concepto de búsqueda> <Concepto de búsqueda> [<cadena de búsqueda> ...]\n"
|
||||
"\n"
|
||||
"Devuelve los resultados de cada búsqueda, en orden, de mayor número\n"
|
||||
"de los resultados a menos.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:247
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<source language> [to] <target language> <text>\n"
|
||||
"\n"
|
||||
" Returns <text> translated from <source language> into <target\n"
|
||||
" language>.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<idioma fuente> [a] <idioma de destino> <texto>\n"
|
||||
"\n"
|
||||
"Devuelve <texto> traducidos de <idioma fuente> a <destino\n"
|
||||
"idioma>.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:285
|
||||
#, docstring
|
||||
msgid "^google\\s+(.*)$"
|
||||
msgstr "^google\\s+(.*)$"
|
||||
|
||||
#: plugin.py:306
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<expression>\n"
|
||||
"\n"
|
||||
" Uses Google's calculator to calculate the value of <expression>.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<expresión>\n"
|
||||
"\n"
|
||||
"Utiliza la calculadora de Google para calcular el valor de <expresión>.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:341
|
||||
#, docstring
|
||||
msgid ""
|
||||
"<phone number>\n"
|
||||
"\n"
|
||||
" Looks <phone number> up on Google.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<número de teléfono>\n"
|
||||
"\n"
|
||||
"Mirar <número de teléfono> en Google.\n"
|
||||
" "
|
||||
|
||||
#: plugin.py:358
|
||||
msgid "Google's phonebook didn't come up with anything."
|
||||
msgstr "Guía de teléfonos de Google no llegó a nada."
|
91
plugins/Poll/README.rst
Normal file
91
plugins/Poll/README.rst
Normal file
@ -0,0 +1,91 @@
|
||||
.. _plugin-Poll:
|
||||
|
||||
Documentation for the Poll plugin for Supybot
|
||||
=============================================
|
||||
|
||||
Purpose
|
||||
-------
|
||||
Poll: Provides a simple way to vote on answers to a question
|
||||
|
||||
Usage
|
||||
-----
|
||||
Provides a simple way to vote on answers to a question
|
||||
|
||||
For example, this creates a poll::
|
||||
|
||||
<admin> @poll add "Is this a test?" "Yes" "No" "Maybe"
|
||||
<bot> The operation succeeded. Poll # 42 created.
|
||||
|
||||
Creates a poll that can be voted on in this way::
|
||||
|
||||
<citizen1> @vote 42 Yes
|
||||
<citizen2> @vote 42 No
|
||||
<citizen3> @vote 42 No
|
||||
|
||||
And results::
|
||||
|
||||
<admin> @poll results
|
||||
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
|
||||
Longer answers are possible, and voters only need to use the first
|
||||
word of each answer to vote. For example, this creates a poll that
|
||||
can be voted on in the same way::
|
||||
|
||||
<admin> @poll add "Is this a test?" "Yes totally" "No no no" "Maybe"
|
||||
<bot> The operation succeeded. Poll # 43 created.
|
||||
|
||||
You can also add a number or letter at the beginning of each question to
|
||||
make it easier::
|
||||
|
||||
<admin> @poll add "Who is the best captain?" "1 James T Kirk" "2 Jean-Luc Picard" "3 Benjamin Sisko" "4 Kathryn Janeway"
|
||||
<bot> The operation succeeded. Poll # 44 created.
|
||||
|
||||
<trekkie1> @vote 42 1
|
||||
<trekkie2> @vote 42 4
|
||||
<trekkie3> @vote 42 4
|
||||
|
||||
.. _commands-Poll:
|
||||
|
||||
Commands
|
||||
--------
|
||||
.. _command-poll-add:
|
||||
|
||||
add [<channel>] <question> <answer1> [<answer2> [<answer3> [...]]]
|
||||
Creates a new poll with the specified <question> and answers on the <channel>. The first word of each answer is used as its id to vote, so each answer should start with a different word. <channel> is only necessary if this command is run in private, and defaults to the current channel otherwise.
|
||||
|
||||
.. _command-poll-close:
|
||||
|
||||
close [<channel>] <poll_id>
|
||||
Closes the specified poll.
|
||||
|
||||
.. _command-poll-results:
|
||||
|
||||
results [<channel>] <poll_id>
|
||||
Returns the results of the specified poll.
|
||||
|
||||
.. _command-poll-vote:
|
||||
|
||||
vote [<channel>] <poll_id> <answer_id>
|
||||
Registers your vote on the poll <poll_id> as being the answer identified by <answer_id> (which is the first word of each possible answer).
|
||||
|
||||
.. _conf-Poll:
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
.. _conf-supybot.plugins.Poll.public:
|
||||
|
||||
|
||||
supybot.plugins.Poll.public
|
||||
This config variable defaults to "True", is not network-specific, and is not channel-specific.
|
||||
|
||||
Determines whether this plugin is publicly visible.
|
||||
|
||||
.. _conf-supybot.plugins.Poll.requireManageCapability:
|
||||
|
||||
|
||||
supybot.plugins.Poll.requireManageCapability
|
||||
This config variable defaults to "channel,op; channel,halfop", is network-specific, and is channel-specific.
|
||||
|
||||
Determines the capabilities required (if any) to open and close polls. Use 'channel,capab' for channel-level capabilities. Note that absence of an explicit anticapability means user has capability.
|
||||
|
72
plugins/Poll/__init__.py
Normal file
72
plugins/Poll/__init__.py
Normal file
@ -0,0 +1,72 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
###
|
||||
|
||||
"""
|
||||
Poll: Provides a simple way to vote on answers to a question
|
||||
"""
|
||||
|
||||
import sys
|
||||
import supybot
|
||||
from supybot import world
|
||||
|
||||
# Use this for the version of this plugin.
|
||||
__version__ = ""
|
||||
|
||||
# XXX Replace this with an appropriate author or supybot.Author instance.
|
||||
__author__ = supybot.authors.unknown
|
||||
|
||||
# This is a dictionary mapping supybot.Author instances to lists of
|
||||
# contributions.
|
||||
__contributors__ = {}
|
||||
|
||||
# This is a url where the most recent plugin package can be downloaded.
|
||||
__url__ = ""
|
||||
|
||||
from . import config
|
||||
from . import plugin
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib import reload
|
||||
else:
|
||||
from imp import reload
|
||||
# In case we're being reloaded.
|
||||
reload(config)
|
||||
reload(plugin)
|
||||
# Add more reloads here if you add third-party modules and want them to be
|
||||
# reloaded when this plugin is reloaded. Don't forget to import them as well!
|
||||
|
||||
if world.testing:
|
||||
from . import test
|
||||
|
||||
Class = plugin.Class
|
||||
configure = config.configure
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
72
plugins/Poll/config.py
Normal file
72
plugins/Poll/config.py
Normal file
@ -0,0 +1,72 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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 import conf, registry
|
||||
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
|
||||
_ = PluginInternationalization("Poll")
|
||||
except:
|
||||
# Placeholder that allows to run the plugin on a bot
|
||||
# without the i18n module
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
def configure(advanced):
|
||||
# This will be called by supybot to configure this module. advanced is
|
||||
# a bool that specifies whether the user identified themself as an advanced
|
||||
# user or not. You should effect your configuration by manipulating the
|
||||
# registry as appropriate.
|
||||
from supybot.questions import expect, anything, something, yn
|
||||
|
||||
conf.registerPlugin("Poll", True)
|
||||
|
||||
|
||||
Poll = conf.registerPlugin("Poll")
|
||||
# This is where your configuration variables (if any) should go. For example:
|
||||
# conf.registerGlobalValue(Poll, 'someConfigVariableName',
|
||||
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
|
||||
|
||||
conf.registerChannelValue(
|
||||
Poll,
|
||||
"requireManageCapability",
|
||||
registry.String(
|
||||
"channel,op; channel,halfop",
|
||||
_(
|
||||
"""Determines the capabilities required (if any) to open and
|
||||
close polls.
|
||||
Use 'channel,capab' for channel-level capabilities.
|
||||
Note that absence of an explicit anticapability means user has
|
||||
capability.
|
||||
"""
|
||||
),
|
||||
),
|
||||
)
|
1
plugins/Poll/local/__init__.py
Normal file
1
plugins/Poll/local/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Stub so local is a module, used for third-party modules
|
225
plugins/Poll/plugin.py
Normal file
225
plugins/Poll/plugin.py
Normal file
@ -0,0 +1,225 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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 collections
|
||||
import re
|
||||
|
||||
from supybot import utils, plugins, ircdb, ircutils, callbacks
|
||||
from supybot.commands import *
|
||||
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
|
||||
_ = PluginInternationalization("Poll")
|
||||
except ImportError:
|
||||
# Placeholder that allows to run the plugin on a bot
|
||||
# without the i18n module
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
Poll = collections.namedtuple("Poll", "question answers votes open")
|
||||
|
||||
|
||||
class Poll_(callbacks.Plugin):
|
||||
"""Provides a simple way to vote on answers to a question
|
||||
|
||||
For example, this creates a poll::
|
||||
|
||||
<admin> @poll add "Is this a test?" "Yes" "No" "Maybe"
|
||||
<bot> The operation succeeded. Poll # 42 created.
|
||||
|
||||
Creates a poll that can be voted on in this way::
|
||||
|
||||
<citizen1> @vote 42 Yes
|
||||
<citizen2> @vote 42 No
|
||||
<citizen3> @vote 42 No
|
||||
|
||||
And results::
|
||||
|
||||
<admin> @poll results
|
||||
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
|
||||
Longer answers are possible, and voters only need to use the first
|
||||
word of each answer to vote. For example, this creates a poll that
|
||||
can be voted on in the same way::
|
||||
|
||||
<admin> @poll add "Is this a test?" "Yes totally" "No no no" "Maybe"
|
||||
<bot> The operation succeeded. Poll # 43 created.
|
||||
|
||||
You can also add a number or letter at the beginning of each question to
|
||||
make it easier::
|
||||
|
||||
<admin> @poll add "Who is the best captain?" "1 James T Kirk" "2 Jean-Luc Picard" "3 Benjamin Sisko" "4 Kathryn Janeway"
|
||||
<bot> The operation succeeded. Poll # 44 created.
|
||||
|
||||
<trekkie1> @vote 42 1
|
||||
<trekkie2> @vote 42 4
|
||||
<trekkie3> @vote 42 4
|
||||
"""
|
||||
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
|
||||
# {(network, channel): {id: Poll}}
|
||||
self._polls = collections.defaultdict(dict)
|
||||
|
||||
def name(self):
|
||||
return "Poll"
|
||||
|
||||
def _checkManageCapability(self, irc, msg, channel):
|
||||
# Copy-pasted from Topic
|
||||
capabilities = self.registryValue(
|
||||
"requireManageCapability", channel, irc.network
|
||||
)
|
||||
for capability in re.split(r"\s*;\s*", capabilities):
|
||||
if capability.startswith("channel,"):
|
||||
capability = ircdb.makeChannelCapability(
|
||||
channel, capability[8:]
|
||||
)
|
||||
if capability and ircdb.checkCapability(msg.prefix, capability):
|
||||
return
|
||||
irc.errorNoCapability(capabilities, Raise=True)
|
||||
|
||||
def _getPoll(self, irc, channel, poll_id):
|
||||
poll = self._polls[(irc.network, channel)].get(poll_id)
|
||||
if poll is None:
|
||||
irc.error(
|
||||
_("A poll with this ID does not exist in this channel."),
|
||||
Raise=True,
|
||||
)
|
||||
return poll
|
||||
|
||||
@wrap(["channel", "something", many("something")])
|
||||
def add(self, irc, msg, args, channel, question, answers):
|
||||
"""[<channel>] <question> <answer1> [<answer2> [<answer3> [...]]]
|
||||
|
||||
Creates a new poll with the specified <question> and answers
|
||||
on the <channel>.
|
||||
The first word of each answer is used as its id to vote,
|
||||
so each answer should start with a different word.
|
||||
|
||||
<channel> is only necessary if this command is run in private,
|
||||
and defaults to the current channel otherwise."""
|
||||
self._checkManageCapability(irc, msg, channel)
|
||||
|
||||
poll_id = max(self._polls[(irc.network, channel)], default=0) + 1
|
||||
|
||||
answers = [(answer.split()[0], answer) for answer in answers]
|
||||
|
||||
answer_id_counts = collections.Counter(id_ for (id_, _) in answers).items()
|
||||
duplicate_answer_ids = [
|
||||
answer_id for (answer_id, count) in answer_id_counts if count > 1
|
||||
]
|
||||
if duplicate_answer_ids:
|
||||
irc.error(
|
||||
format(
|
||||
_("Duplicate answer identifier(s): %L"), duplicate_answer_ids
|
||||
),
|
||||
Raise=True,
|
||||
)
|
||||
|
||||
self._polls[(irc.network, channel)][poll_id] = Poll(
|
||||
question=question, answers=dict(answers), votes={}, open=True
|
||||
)
|
||||
|
||||
irc.replySuccess(_("Poll # %d created.") % poll_id)
|
||||
|
||||
@wrap(["channel", "positiveInt"])
|
||||
def close(self, irc, msg, args, channel, poll_id):
|
||||
"""[<channel>] <poll_id>
|
||||
|
||||
Closes the specified poll."""
|
||||
self._checkManageCapability(irc, msg, channel)
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
if not poll.open:
|
||||
irc.error(_("This poll was already closed."), Raise=True)
|
||||
|
||||
poll = Poll(
|
||||
question=poll.question,
|
||||
answers=poll.answers,
|
||||
votes=poll.votes,
|
||||
open=False,
|
||||
)
|
||||
self._polls[(irc.network, channel)][poll_id] = poll
|
||||
irc.replySuccess()
|
||||
|
||||
@wrap(["channel", "positiveInt", "somethingWithoutSpaces"])
|
||||
def vote(self, irc, msg, args, channel, poll_id, answer_id):
|
||||
"""[<channel>] <poll_id> <answer_id>
|
||||
|
||||
Registers your vote on the poll <poll_id> as being the answer
|
||||
identified by <answer_id> (which is the first word of each possible
|
||||
answer)."""
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
if not poll.open:
|
||||
irc.error(_("This poll is closed."), Raise=True)
|
||||
|
||||
if msg.nick in poll.votes:
|
||||
irc.error(_("You already voted on this poll."), Raise=True)
|
||||
|
||||
if answer_id not in poll.answers:
|
||||
irc.error(
|
||||
format(
|
||||
_("Invalid answer ID. Valid answers are: %L"),
|
||||
poll.answers,
|
||||
),
|
||||
Raise=True,
|
||||
)
|
||||
|
||||
poll.votes[msg.nick] = answer_id
|
||||
|
||||
irc.replySuccess()
|
||||
|
||||
@wrap(["channel", "positiveInt"])
|
||||
def results(self, irc, msg, args, channel, poll_id):
|
||||
"""[<channel>] <poll_id>
|
||||
|
||||
Returns the results of the specified poll."""
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
counts = collections.Counter(poll.votes.values())
|
||||
|
||||
# Add answers with 0 votes
|
||||
counts.update({answer_id: 0 for answer_id in poll.answers})
|
||||
|
||||
results = [
|
||||
format(_("%n for %s"), (v, "vote"), k)
|
||||
for (k, v) in counts.most_common()
|
||||
]
|
||||
|
||||
irc.replies(results)
|
||||
|
||||
|
||||
Class = Poll_
|
124
plugins/Poll/test.py
Normal file
124
plugins/Poll/test.py
Normal file
@ -0,0 +1,124 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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 *
|
||||
|
||||
|
||||
class PollTestCase(ChannelPluginTestCase):
|
||||
plugins = ("Poll",)
|
||||
|
||||
def testBasics(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter3!foo@bar")
|
||||
|
||||
self.assertResponse(
|
||||
"results 1",
|
||||
"2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testDoubleVoting(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertResponse(
|
||||
"vote 1 Yes",
|
||||
"voter1: Error: You already voted on this poll.",
|
||||
frm="voter1!foo@bar",
|
||||
)
|
||||
|
||||
self.assertRegexp(
|
||||
"results 1",
|
||||
"1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testClosed(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("close 1")
|
||||
self.assertResponse(
|
||||
"vote 1 Yes",
|
||||
"voter3: Error: This poll is closed.",
|
||||
frm="voter3!foo@bar",
|
||||
)
|
||||
self.assertRegexp("close 1", "already closed")
|
||||
|
||||
self.assertRegexp(
|
||||
"results 1",
|
||||
"1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testNonExisting(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertRegexp("vote 2 Yes", "does not exist")
|
||||
|
||||
def testLongAnswers(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes totally" "No no no" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter3!foo@bar")
|
||||
|
||||
self.assertResponse(
|
||||
"results 1",
|
||||
"2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testDuplicateId(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "Yes" "Maybe"',
|
||||
"Error: Duplicate answer identifier(s): Yes",
|
||||
)
|
||||
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"',
|
||||
"Error: Duplicate answer identifier(s): Yes",
|
||||
)
|
@ -23,6 +23,11 @@ necessary if the bot is properly configured.
|
||||
|
||||
Commands
|
||||
--------
|
||||
.. _command-services-chanserv:
|
||||
|
||||
chanserv <text>
|
||||
Sends the <text> to ChanServ. For example, to register a channel on Atheme, use: @chanserv REGISTER <#channel>.
|
||||
|
||||
.. _command-services-ghost:
|
||||
|
||||
ghost [<nick>]
|
||||
@ -43,6 +48,11 @@ invite [<channel>]
|
||||
nicks takes no arguments
|
||||
Returns the nicks that this plugin is configured to identify and ghost with.
|
||||
|
||||
.. _command-services-nickserv:
|
||||
|
||||
nickserv <text>
|
||||
Sends the <text> to NickServ. For example, to register to NickServ on Atheme, use: @nickserv REGISTER <password> <email-address>.
|
||||
|
||||
.. _command-services-op:
|
||||
|
||||
op [<channel>]
|
||||
|
@ -558,6 +558,35 @@ class Services(callbacks.Plugin):
|
||||
'I\'m able to ghost a nick.'))
|
||||
ghost = wrap(ghost, [('checkCapability', 'admin'), additional('nick')])
|
||||
|
||||
def nickserv(self, irc, msg, args, text):
|
||||
"""<text>
|
||||
|
||||
Sends the <text> to NickServ. For example, to register to NickServ
|
||||
on Atheme, use: @nickserv REGISTER <password> <email-address>."""
|
||||
nickserv = self.registryValue('NickServ', network=irc.network)
|
||||
if nickserv:
|
||||
irc.replySuccess()
|
||||
irc.queueMsg(ircmsgs.privmsg(nickserv, text))
|
||||
else:
|
||||
irc.error(_('You must set supybot.plugins.Services.NickServ before '
|
||||
'I\'m able to message NickServ'))
|
||||
nickserv = wrap(nickserv, ['owner', 'text'])
|
||||
|
||||
def chanserv(self, irc, msg, args, text):
|
||||
"""<text>
|
||||
|
||||
Sends the <text> to ChanServ. For example, to register a channel
|
||||
on Atheme, use: @chanserv REGISTER <#channel>."""
|
||||
chanserv = self.registryValue('ChanServ', network=irc.network)
|
||||
if chanserv:
|
||||
irc.replySuccess()
|
||||
irc.queueMsg(ircmsgs.privmsg(chanserv, text))
|
||||
else:
|
||||
irc.error(_('You must set supybot.plugins.Services.ChanServ before '
|
||||
'I\'m able to message ChanServ'))
|
||||
chanserv = wrap(chanserv, ['owner', 'text'])
|
||||
|
||||
|
||||
@internationalizeDocstring
|
||||
def password(self, irc, msg, args, nick, password):
|
||||
"""<nick> [<password>]
|
||||
@ -606,9 +635,9 @@ class Services(callbacks.Plugin):
|
||||
Raise=True
|
||||
)
|
||||
|
||||
if "draft/register" not in otherIrc.state.capabilities_ls:
|
||||
if "draft/account-registration" not in otherIrc.state.capabilities_ls:
|
||||
irc.error(
|
||||
_("This network does not support draft/register."),
|
||||
_("This network does not support draft/account-registration."),
|
||||
Raise=True
|
||||
)
|
||||
|
||||
@ -636,7 +665,7 @@ class Services(callbacks.Plugin):
|
||||
# https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495
|
||||
self._checkCanRegister(irc, otherIrc)
|
||||
|
||||
cap_values = (otherIrc.state.capabilities_ls["draft/register"] or "").split(",")
|
||||
cap_values = (otherIrc.state.capabilities_ls["draft/account-registration"] or "").split(",")
|
||||
if "email-required" in cap_values and email is None:
|
||||
irc.error(
|
||||
_("This network requires an email address to register."),
|
||||
@ -648,7 +677,7 @@ class Services(callbacks.Plugin):
|
||||
otherIrc.queueMsg(ircmsgs.IrcMsg(
|
||||
server_tags={"label": label},
|
||||
command="REGISTER",
|
||||
args=[email or "*", password],
|
||||
args=["*", email or "*", password],
|
||||
))
|
||||
register = wrap(register, ["owner", "private", "networkIrc", "something", optional("email")])
|
||||
|
||||
|
@ -88,6 +88,18 @@ class ServicesTestCase(PluginTestCase):
|
||||
finally:
|
||||
self.assertNotError('services password %s ""' % self.nick)
|
||||
|
||||
def testNickserv(self):
|
||||
self.assertNotError('nickserv foo bar')
|
||||
m = self.irc.takeMsg()
|
||||
self.assertEqual(m.command, 'PRIVMSG', m)
|
||||
self.assertEqual(m.args, ('NickServ', 'foo bar'), m)
|
||||
|
||||
def testChanserv(self):
|
||||
self.assertNotError('chanserv foo bar')
|
||||
m = self.irc.takeMsg()
|
||||
self.assertEqual(m.command, 'PRIVMSG', m)
|
||||
self.assertEqual(m.args, ('ChanServ', 'foo bar'), m)
|
||||
|
||||
def testRegisterNoExperimentalExtensions(self):
|
||||
self.assertRegexp(
|
||||
"register p4ssw0rd", "error: Experimental IRC extensions")
|
||||
@ -180,7 +192,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
super().setUp()
|
||||
conf.supybot.protocols.irc.experimentalExtensions.setValue(True)
|
||||
self._initialCaps = self.irc.state.capabilities_ls.copy()
|
||||
self.irc.state.capabilities_ls["draft/register"] = None
|
||||
self.irc.state.capabilities_ls["draft/account-registration"] = None
|
||||
self.irc.state.capabilities_ls["labeled-response"] = None
|
||||
|
||||
def tearDown(self):
|
||||
@ -196,17 +208,17 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
"register p4ssw0rd",
|
||||
"error: This network does not support labeled-response.")
|
||||
|
||||
del self.irc.state.capabilities_ls["draft/register"]
|
||||
del self.irc.state.capabilities_ls["draft/account-registration"]
|
||||
self.assertRegexp(
|
||||
"register p4ssw0rd",
|
||||
"error: This network does not support draft/register.")
|
||||
"error: This network does not support draft/account-registration.")
|
||||
finally:
|
||||
self.irc.state.capabilities_ls = old_caps
|
||||
|
||||
def testRegisterRequireEmail(self):
|
||||
old_caps = self.irc.state.capabilities_ls.copy()
|
||||
try:
|
||||
self.irc.state.capabilities_ls["draft/register"] = "email-required"
|
||||
self.irc.state.capabilities_ls["draft/account-registration"] = "email-required"
|
||||
self.assertRegexp(
|
||||
"register p4ssw0rd",
|
||||
"error: This network requires an email address to register.")
|
||||
@ -216,7 +228,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
def testRegisterSuccess(self):
|
||||
m = self.getMsg("register p4ssw0rd")
|
||||
label = m.server_tags.pop("label")
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"]))
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"]))
|
||||
self.irc.feedMsg(IrcMsg(
|
||||
server_tags={"label": label},
|
||||
command="REGISTER",
|
||||
@ -230,7 +242,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
# oragono replies with a batch
|
||||
m = self.getMsg("register p4ssw0rd")
|
||||
label = m.server_tags.pop("label")
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"]))
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"]))
|
||||
|
||||
batch_name = "Services_testRegisterSuccessBatch"
|
||||
self.irc.feedMsg(IrcMsg(
|
||||
@ -261,7 +273,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
m = self.getMsg("register p4ssw0rd foo@example.org")
|
||||
label = m.server_tags.pop("label")
|
||||
self.assertEqual(m, IrcMsg(
|
||||
command="REGISTER", args=["foo@example.org", "p4ssw0rd"]))
|
||||
command="REGISTER", args=["*", "foo@example.org", "p4ssw0rd"]))
|
||||
self.irc.feedMsg(IrcMsg(
|
||||
server_tags={"label": label},
|
||||
command="REGISTER",
|
||||
@ -274,7 +286,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
def testRegisterVerify(self):
|
||||
m = self.getMsg("register p4ssw0rd")
|
||||
label = m.server_tags.pop("label")
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"]))
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"]))
|
||||
self.irc.feedMsg(IrcMsg(
|
||||
server_tags={"label": label},
|
||||
command="REGISTER",
|
||||
@ -301,7 +313,7 @@ class ExperimentalServicesTestCase(PluginTestCase):
|
||||
def testRegisterVerifyBatch(self):
|
||||
m = self.getMsg("register p4ssw0rd")
|
||||
label = m.server_tags.pop("label")
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"]))
|
||||
self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"]))
|
||||
self.irc.feedMsg(IrcMsg(
|
||||
server_tags={"label": label},
|
||||
command="REGISTER",
|
||||
|
@ -60,12 +60,12 @@ list [--capability=<capability>] [<glob>]
|
||||
.. _command-user-register:
|
||||
|
||||
register <name> <password>
|
||||
Registers <name> with the given password <password> and the current hostmask of the person registering. You shouldn't register twice; if you're not recognized as a user but you've already registered, use the hostmask add command to add another hostmask to your already-registered user, or use the identify command to identify just for a session. This command (and all other commands that include a password) must be sent to the bot privately, not in a channel.
|
||||
Registers <name> with the given password <password> and the current hostmask of the person registering. You shouldn't register twice; if you're not recognized as a user but you've already registered, use the hostmask add command to add another hostmask to your already-registered user, or use the identify command to identify just for a session. This command (and all other commands that include a password) must be sent to the bot privately, not in a channel. Use "!" instead of <password> to disable password authentication.
|
||||
|
||||
.. _command-user-set.password:
|
||||
|
||||
set password [<name>] <old password> <new password>
|
||||
Sets the new password for the user specified by <name> to <new password>. Obviously this message must be sent to the bot privately (not in a channel). If the requesting user is an owner user, then <old password> needn't be correct.
|
||||
Sets the new password for the user specified by <name> to <new password>. Obviously this message must be sent to the bot privately (not in a channel). If the requesting user is an owner user, then <old password> needn't be correct. If the <new password> is "!", password login will be disabled.
|
||||
|
||||
.. _command-user-set.secure:
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
include = 'plugins/(Autocomplete|Fediverse)/.*\.pyi?$'
|
||||
include = 'plugins/(Autocomplete|Fediverse|Poll)/.*\.pyi?$'
|
||||
|
@ -84,7 +84,7 @@ class PluginDoc(object):
|
||||
def __init__(self, mod, titleTemplate):
|
||||
self.mod = mod
|
||||
self.inst = self.mod.Class(None)
|
||||
self.name = self.mod.Class.__name__
|
||||
self.name = self.inst.name()
|
||||
self.appendExtraBlankLine = False
|
||||
self.titleTemplate = string.Template(titleTemplate)
|
||||
self.lines = []
|
||||
|
@ -511,7 +511,9 @@ class RichReplyMethods(object):
|
||||
# change the state of this Irc object.
|
||||
if to is not None:
|
||||
self.to = self.to or to
|
||||
if self.private or self.msg.channel is None:
|
||||
if self.private:
|
||||
target = to or self.msg.nick
|
||||
elif self.msg.channel is None:
|
||||
target = self.msg.nick
|
||||
else:
|
||||
target = self.to or self.msg.args[0]
|
||||
@ -721,21 +723,26 @@ class ReplyIrcProxy(RichReplyMethods):
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.irc, attr)
|
||||
|
||||
def _replyOverhead(self, target, targetNick, prefixNick):
|
||||
def _replyOverhead(self, msg, **kwargs):
|
||||
"""Returns the number of bytes added to a PRIVMSG payload, either by
|
||||
Limnoria itself or by the server.
|
||||
Ignores tag bytes, as they are accounted for separatly."""
|
||||
overhead = (
|
||||
len(':')
|
||||
+ len(self.irc.prefix.encode())
|
||||
+ len(' PRIVMSG ')
|
||||
+ len(target.encode())
|
||||
+ len(' :')
|
||||
+ len('\r\n')
|
||||
)
|
||||
if prefixNick and targetNick is not None:
|
||||
overhead += len(targetNick) + len(': ')
|
||||
return overhead
|
||||
Ignores tag bytes, as they are accounted for separately."""
|
||||
# FIXME: big hack.
|
||||
# _makeReply does a lot of internal state computation, especially
|
||||
# related to the final target to use.
|
||||
# I tried to get them out of _makeReply but it's a clusterfuck, so I
|
||||
# gave up. Instead, we call _makeReply with a dummy payload to guess
|
||||
# what overhead it will add.
|
||||
payload = 'foo'
|
||||
channel = msg.channel
|
||||
msg = copy.deepcopy(msg) # because _makeReply calls .tag('repliedTo')
|
||||
msg.channel = channel # ugh... copy.deepcopy uses msg.__reduce__
|
||||
reply_msg = _makeReply(self, msg, payload, **kwargs)
|
||||
|
||||
# strip tags, add prefix
|
||||
reply_msg = ircmsgs.IrcMsg(
|
||||
msg=reply_msg, server_tags={}, prefix=self.prefix)
|
||||
return len(str(reply_msg)) - len(payload)
|
||||
|
||||
def _sendReply(self, s, target, msg, sendImmediately=False,
|
||||
noLengthCheck=False, **kwargs):
|
||||
@ -760,8 +767,7 @@ class ReplyIrcProxy(RichReplyMethods):
|
||||
allowedLength = conf.get(conf.supybot.reply.mores.length,
|
||||
channel=target, network=self.irc.network)
|
||||
if not allowedLength: # 0 indicates this.
|
||||
allowedLength = 512 - self._replyOverhead(
|
||||
target, msg.nick, prefixNick=kwargs['prefixNick'])
|
||||
allowedLength = 512 - self._replyOverhead(msg, **kwargs)
|
||||
maximumMores = conf.get(conf.supybot.reply.mores.maximum,
|
||||
channel=target, network=self.irc.network)
|
||||
maximumLength = allowedLength * maximumMores
|
||||
@ -901,12 +907,12 @@ class ReplyIrcProxy(RichReplyMethods):
|
||||
assert conf.supybot.protocols.irc.experimentalExtensions()
|
||||
assert 'draft/multiline' in self.state.capabilities_ack
|
||||
|
||||
if not allowedLength: # 0 indicates this.
|
||||
# We're only interested in the overhead outside the payload,
|
||||
# regardless of the entire payload (nick prefix included),
|
||||
# so prefixNick=False
|
||||
allowedLength = 512 - self._replyOverhead(
|
||||
target, targetNick, prefixNick=False)
|
||||
if allowedLength: # 0 indicates this.
|
||||
largest_msg_size = allowedLength
|
||||
else:
|
||||
# Used as upper bound of each message's size to decide how many
|
||||
# messages to put in each batch.
|
||||
largest_msg_size = max(len(msg.args[1]) for msg in msgs)
|
||||
|
||||
multiline_cap_values = ircutils.parseCapabilityKeyValue(
|
||||
self.state.capabilities_ls['draft/multiline'])
|
||||
@ -919,7 +925,7 @@ class ReplyIrcProxy(RichReplyMethods):
|
||||
# encode messages again here just to have their length, so
|
||||
# let's assume they all have the maximum length.
|
||||
# It's not optimal, but close enough and simplifies the code.
|
||||
messages_per_batch = max_bytes_per_batch // allowedLength
|
||||
messages_per_batch = max_bytes_per_batch // largest_msg_size
|
||||
|
||||
# "Clients MUST NOT send tags other than draft/multiline-concat and
|
||||
# batch on messages within the batch. In particular, all client-only
|
||||
@ -1139,7 +1145,7 @@ class NestedCommandsIrcProxy(ReplyIrcProxy):
|
||||
# evaluated our own list of arguments.
|
||||
assert not self.finalEvaled, 'finalEval called twice.'
|
||||
self.finalEvaled = True
|
||||
# Now, the way we call a command is we iterate over the loaded pluings,
|
||||
# Now, the way we call a command is we iterate over the loaded plugins,
|
||||
# asking each one if the list of args we have interests it. The
|
||||
# way we do that is by calling getCommand on the plugin.
|
||||
# The plugin will return a list of args which it considers to be
|
||||
|
11
src/conf.py
11
src/conf.py
@ -339,10 +339,19 @@ class SpaceSeparatedSetOfChannels(registry.SpaceSeparatedListOf):
|
||||
channels.append(channel)
|
||||
msg = ircmsgs.joins(channels_with_key + channels, keys)
|
||||
if len(str(msg)) > 512:
|
||||
# Use previous short enough join message
|
||||
msgs.append(old)
|
||||
# Reset and construct a new join message using the current
|
||||
# channel.
|
||||
keys = []
|
||||
channels_with_key = []
|
||||
channels = []
|
||||
if key:
|
||||
keys.append(key)
|
||||
channels_with_key.append(channel)
|
||||
else:
|
||||
channels.append(channel)
|
||||
msg = ircmsgs.joins(channels_with_key + channels, keys)
|
||||
old = msg
|
||||
if msg:
|
||||
msgs.append(msg)
|
||||
@ -554,7 +563,7 @@ registerChannelValue(supybot.reply.error, 'withNotice',
|
||||
messages to users via NOTICE instead of PRIVMSG. You might want to do this
|
||||
so users can ignore NOTICEs from the bot and not have to see error
|
||||
messages; or you might want to use it in combination with
|
||||
supybot.reply.errorInPrivate so private errors don't open a query window
|
||||
supybot.reply.error.inPrivate so private errors don't open a query window
|
||||
in most IRC clients.""")))
|
||||
registerChannelValue(supybot.reply.error, 'noCapability',
|
||||
registry.Boolean(False, _("""Determines whether the bot will *not* provide
|
||||
|
@ -318,7 +318,7 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
|
||||
# address is a hostname, eg. because we're using a SOCKS
|
||||
# proxy
|
||||
is_loopback = False
|
||||
if not is_loopback:
|
||||
if not is_loopback and not address.endswith('.onion'):
|
||||
drivers.log.warning(('Connection to network %s '
|
||||
'does not use SSL/TLS, which makes it vulnerable to '
|
||||
'man-in-the-middle attacks and passive eavesdropping. '
|
||||
|
@ -61,7 +61,7 @@ class IrcDriver(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def die(self):
|
||||
# The end of any overrided die method should be
|
||||
# The end of any overridden die method should be
|
||||
# "super(Class, self).die()", in order to make
|
||||
# sure this (and anything else later added) is done.
|
||||
remove(self.name())
|
||||
|
@ -708,8 +708,11 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
|
||||
self.channels = channels
|
||||
self.nicksToHostmasks = nicksToHostmasks
|
||||
|
||||
# Batches should always finish and be way shorter than 3600s, but
|
||||
# let's just make sure to avoid leaking memory.
|
||||
# Batches usually finish and are way shorter than 3600s, but
|
||||
# we need to:
|
||||
# * keep them in case the connection breaks (and reset() can't
|
||||
# clear the list itself)
|
||||
# * make sure to avoid leaking memory in general
|
||||
self.batches = ExpiringDict(timeout=3600)
|
||||
|
||||
def reset(self):
|
||||
@ -721,12 +724,24 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
|
||||
self.channels.clear()
|
||||
self.supported.clear()
|
||||
self.nicksToHostmasks.clear()
|
||||
self.batches.clear()
|
||||
self.capabilities_req = set()
|
||||
self.capabilities_ack = set()
|
||||
self.capabilities_nak = set()
|
||||
self.capabilities_ls = {}
|
||||
|
||||
# Don't clear batches right now. reset() is called on ERROR messages,
|
||||
# which may be part of a BATCH so we need to remember that batch.
|
||||
# At worst, the batch will expire in the near future, as self.batches
|
||||
# is an instance of ExpiringDict.
|
||||
# If we did clear the batch, then this would happen:
|
||||
# 1. IrcState.addMsg() would crash on the ERROR, because its batch
|
||||
# server-tag references an unknown batch, so it would not set the
|
||||
# 'batch' supybot-tag
|
||||
# 2. Irc.doBatch would crash on the closing BATCH, for the same reason
|
||||
# 3. Owner.doBatch would crash because it expects the batch
|
||||
# supybot-tag to be set, but it wasn't because of 1
|
||||
#self.batches.clear()
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self.history, self.supported,
|
||||
self.nicksToHostmasks, self.channels))
|
||||
|
@ -272,7 +272,10 @@ class IrcMsg(object):
|
||||
else:
|
||||
self.reply_env = None
|
||||
self.tags = msg.tags.copy()
|
||||
self.server_tags = msg.server_tags
|
||||
if server_tags is None:
|
||||
self.server_tags = msg.server_tags.copy()
|
||||
else:
|
||||
self.server_tags = server_tags
|
||||
self.time = msg.time
|
||||
else:
|
||||
self.prefix = prefix
|
||||
|
@ -445,7 +445,7 @@ class Value(Group):
|
||||
# The complicated case. We want a net+chan specific value,
|
||||
# which may come in three different ways:
|
||||
#
|
||||
# 1. it was set explicitely net+chan
|
||||
# 1. it was set explicitly net+chan
|
||||
# 2. it's inherited from a net specific value (which may itself be
|
||||
# inherited from the base value)
|
||||
# 3. it's inherited from the chan specific value (which is not a
|
||||
@ -453,7 +453,7 @@ class Value(Group):
|
||||
# load configuration from old bots).
|
||||
#
|
||||
# The choice between 2 and 3 is done by checking which of the
|
||||
# net-specific and chan-specific values was set explicitely by
|
||||
# net-specific and chan-specific values was set explicitly by
|
||||
# a user/admin. In case both were, the net-specific value is used
|
||||
# (there is no particular reason for this, I just think it makes
|
||||
# more sense).
|
||||
|
@ -561,6 +561,14 @@ class PrivmsgTestCase(ChannelPluginTestCase):
|
||||
" " + "foo " * 79 + "'")
|
||||
self.assertNoResponse(" ", timeout=0.1)
|
||||
|
||||
def testReplyPrivate(self):
|
||||
# Send from a very long nick, which should be taken into account when
|
||||
# computing the reply overhead.
|
||||
self.assertResponse(
|
||||
"eval irc.reply('foo '*300, private=True)",
|
||||
"foo " * 39 + "\x02(7 more messages)\x02",
|
||||
frm='foo'*100 + '!bar@baz')
|
||||
|
||||
def testClientTagReply(self):
|
||||
self.irc.addCallback(self.First(self.irc))
|
||||
|
||||
|
59
test/test_conf.py
Normal file
59
test/test_conf.py
Normal file
@ -0,0 +1,59 @@
|
||||
##
|
||||
# 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 supybot.conf as conf
|
||||
import supybot.registry as registry
|
||||
import supybot.ircutils as ircutils
|
||||
|
||||
|
||||
class SupyConfTestCase(SupyTestCase):
|
||||
def testJoinToOneChannel(self):
|
||||
orig = conf.supybot.networks.test.channels()
|
||||
channels = ircutils.IrcSet()
|
||||
channels.add("#bar")
|
||||
conf.supybot.networks.test.channels.setValue(channels)
|
||||
msgs = conf.supybot.networks.test.channels.joins()
|
||||
self.assertEqual(msgs[0].args, ("#bar",))
|
||||
conf.supybot.networks.test.channels.setValue(orig)
|
||||
|
||||
def testJoinToManyChannels(self):
|
||||
orig = conf.supybot.networks.test.channels()
|
||||
channels = ircutils.IrcSet()
|
||||
input_list = []
|
||||
for x in range(1, 30):
|
||||
name = "#verylongchannelname" + str(x)
|
||||
channels.add(name)
|
||||
input_list.append(name)
|
||||
conf.supybot.networks.test.channels.setValue(channels)
|
||||
msgs = conf.supybot.networks.test.channels.joins()
|
||||
# Double check we split the messages
|
||||
self.assertEqual(len(msgs), 2)
|
||||
# Ensure all channel names are present
|
||||
chan_list = (msgs[0].args[0] + ',' + msgs[1].args[0]).split(',')
|
||||
self.assertCountEqual(input_list, chan_list)
|
||||
conf.supybot.networks.test.channels.setValue(orig)
|
@ -1042,8 +1042,6 @@ class IrcTestCase(SupyTestCase):
|
||||
repr(c.batch)
|
||||
)
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def testBatchNested(self):
|
||||
self.irc.reset()
|
||||
logs = textwrap.dedent('''
|
||||
@ -1107,6 +1105,42 @@ class IrcTestCase(SupyTestCase):
|
||||
self.assertEqual(msg5.tagged('batch'), outer)
|
||||
self.assertEqual(self.irc.state.getParentBatches(msg5), [outer])
|
||||
|
||||
def testBatchError(self):
|
||||
# Checks getting an ERROR message in a batch does not cause issues
|
||||
# due to deinitializing the connection at the same time.
|
||||
self.irc.reset()
|
||||
m1 = ircmsgs.IrcMsg(':host BATCH +name batchtype')
|
||||
self.irc.feedMsg(m1)
|
||||
m2 = ircmsgs.IrcMsg('@batch=name :someuser2 NOTICE * :oh no')
|
||||
self.irc.feedMsg(m2)
|
||||
m3 = ircmsgs.IrcMsg('@batch=name ERROR :bye')
|
||||
self.irc.feedMsg(m3)
|
||||
class Callback(irclib.IrcCallback):
|
||||
batch = None
|
||||
def __call__(self, *args, **kwargs):
|
||||
return super().__call__(*args, **kwargs)
|
||||
def name(self):
|
||||
return 'testcallback'
|
||||
def doBatch(self2, irc, msg):
|
||||
self2.batch = msg.tagged('batch')
|
||||
|
||||
# would usually be called by the driver upon reconnect() trigged
|
||||
# by the ERROR:
|
||||
self.irc.reset()
|
||||
|
||||
c = Callback()
|
||||
self.irc.addCallback(c)
|
||||
try:
|
||||
m4 = ircmsgs.IrcMsg(':host BATCH -name')
|
||||
self.irc.feedMsg(m4)
|
||||
finally:
|
||||
self.irc.removeCallback(c.name())
|
||||
self.assertEqual(
|
||||
c.batch,
|
||||
irclib.Batch('name', 'batchtype', (), [m1, m2, m3, m4], None),
|
||||
repr(c.batch)
|
||||
)
|
||||
|
||||
def testTruncate(self):
|
||||
self.irc = irclib.Irc('test')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user