Compare commits

...

14 Commits

Author SHA1 Message Date
1952dae6f7 src/conf.py: fix typo in supybot.reply.error.withNotice
Co-authored-by: MetaNova <MetaNova@users.noreply.github.com>
Co-authored-by: Val Lorentz <progval+github@progval.net>
2021-06-26 01:23:36 +02:00
James Lu
a7216d290f Remove Spanish translations as requested by the author 2021-06-24 22:36:04 -07:00
Tim Gates
649048443e
Fix a few simple typos (#1476)
Closes #1475
2021-06-21 01:07:50 +02:00
Valentin Lorentz
65ab65cbb1 irclib: Fix crashes when ERROR is part of a batch. 2021-06-20 23:59:51 +02:00
Valentin Lorentz
a7c4c9bd78 Poll: Document usage. 2021-06-19 16:56:17 +02:00
Valentin Lorentz
936d7ebfea Poll Disallow 0 as poll id. 2021-06-19 16:48:40 +02:00
Valentin Lorentz
d919e2133d Poll: Initial commit with basic features. 2021-06-19 16:44:21 +02:00
Valentin Lorentz
3b25a94b46 Regenerate READMEs. 2021-06-19 16:44:21 +02:00
Valentin Lorentz
2293d1c129 Services: Update to the latest version of the draft/account-registration spec. 2021-06-15 20:35:55 +02:00
Clark Boylan
67a39a3adb
Fix joins to many channels (#1473)
* Fix joins to many channels

If you have enough channels that the 512 byte message limit on the JOIN
message is hit then limnoria was losing the channel that put it over the
limit and not including it in the next JOIN message. This resulted in
losing one channel for every JOIN message that pushed us over 512 bytes.

We fix this by generating the JOIN message immediately after resetting
the channels list to ensure we include the channel that pushed us over
the limit. Then the next time through our JOIN msg construction we'll
add subsequent channels without forgetting the one that pushed us over.

* Add test for channel join lists

This adds a test for the issue that is fixed in the previous commit. We
ensure that when JOINs are split over multiple messages we JOIN to all
channels that were part of the input list and don't forget any of them.
2021-06-14 23:15:43 +02:00
Valentin Lorentz
4b82934131 Services: Add @nickserv and @chanserv command, to message services directly
This is because the recommended method ('owner ircquote nickserv register mypassword bot@example.com')
does not work on charybdis, as Limnoria inserts a colon
before the trailing argument and Charybdis' m_alias module
does not parse commands using the IRC syntax, so it
considers the leading colon to be part of the email address.

The alternative would be to change the recommended command to:
'owner ircquote PRIVMSG nickserv :register mypassword bot@example.com'
but it is prone to typos, so I think we should avoid it.
2021-06-14 21:47:36 +02:00
Valentin Lorentz
c8053dad54 Socket: Silence TLS warning for Tor hidden services.
They are already end-to-end encrypted and don't need TLS.
2021-06-12 21:03:15 +02:00
Valentin Lorentz
69c948bd5f irclib: fix _getTarget when to= is given and self.private=True 2021-06-08 21:56:42 +02:00
Valentin Lorentz
8a52902727 irclib: Fix overhead computation by using the real target computation algo 2021-06-08 21:56:08 +02:00
30 changed files with 827 additions and 1261 deletions

View File

@ -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"
" "

View File

@ -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"
" "

View File

@ -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"
" "

View File

@ -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."

View File

@ -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):

View File

@ -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"
" "

View File

@ -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

View File

@ -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
View 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
View 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
View 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.
"""
),
),
)

View File

@ -0,0 +1 @@
# Stub so local is a module, used for third-party modules

225
plugins/Poll/plugin.py Normal file
View 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
View 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",
)

View File

@ -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>]

View File

@ -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")])

View File

@ -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",

View File

@ -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:

View File

@ -1,4 +1,4 @@
[tool.black]
line-length = 79
include = 'plugins/(Autocomplete|Fediverse)/.*\.pyi?$'
include = 'plugins/(Autocomplete|Fediverse|Poll)/.*\.pyi?$'

View File

@ -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 = []

View File

@ -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

View File

@ -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

View File

@ -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. '

View File

@ -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())

View File

@ -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))

View File

@ -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

View File

@ -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).

View File

@ -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
View 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)

View File

@ -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')