├── .gitignore ├── 8ball.py ├── README.md ├── ai.py ├── bomb.py ├── calibre.py ├── debug.py ├── dicelog.py ├── document.py ├── fuckingweather.py ├── helpbot.py ├── imgur.py ├── lart.py ├── multimessage.py ├── nws.py ├── oblique.py ├── redmine.py ├── roulette.py ├── slap.py ├── twit.py └── whois.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /8ball.py: -------------------------------------------------------------------------------- 1 | """ 2 | 8ball.py - Ask the magic 8ball a question 3 | Copyright 2013, Sander Brand http://brantje.com 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.dfbta.net 7 | """ 8 | import sopel 9 | import random 10 | @sopel.module.commands('8') 11 | def ball(bot, trigger): 12 | """Ask the magic 8ball a question! Usage: .8 """ 13 | messages = ["It is certain"," It is decidedly so","Without a doubt","Yes definitely","You may rely on it","As I see it yes","Most likely","Outlook good","Yes","Signs point to yes","Reply hazy try again","Ask again later","Better not tell you now","Cannot predict now","Concentrate and ask again","Don't count on it","My reply is no","God says no","Very doubtful","Outlook not so good"] 14 | answer = random.randint(0,len(messages) - 1) 15 | bot.say(messages[answer]); 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Introduction 2 | 3 | This repository holds modules for [Willie](https://github.com/embolalia/willie) 4 | which, for whatever reason, we don't want to include in the main repository. It 5 | may be that they are used for some specific purpose on [NFIRC](http://dftba.net) 6 | which doesn't have the broad use that makes it worth putting in Willie. It may 7 | be that the module conflicts with another module in Willie in some way, so it's 8 | been removed to require explicitly adding it and running a separate instance. It 9 | may be that the module is new, experimental, or just broken. It may be some 10 | other reason. 11 | 12 | #Instructions 13 | 14 | The easiest way to install these is to put them in ``~/.willie/modules``, and 15 | then add ``extra = /home/yourname/.willie/modules`` to the ``[core]`` section of 16 | your config file. 17 | 18 | If any one module has further instructions, there will (probably) be a file 19 | named something like ``modulename-README.md`` to detail them. 20 | 21 | #Copying 22 | 23 | Each file is licensed individually. If no license is stated, the Eiffel Forum 24 | License v2, below, can be assumed. 25 | 26 | Eiffel Forum License, version 2 27 | 28 | 1. Permission is hereby granted to use, copy, modify and/or distribute this 29 | package, provided that: 30 | * copyright notices are retained unchanged, 31 | * any distribution of this package, whether modified or not, includes this license text. 32 | 33 | 2. Permission is hereby also granted to distribute binary programs 34 | which depend on this package. If the binary program depends on a 35 | modified version of this package, you are encouraged to publicly 36 | release the modified version of this package. 37 | 38 | *********************** 39 | 40 | THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT WARRANTY. ANY EXPRESS OR 41 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 42 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 43 | DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY PARTY FOR ANY 44 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 45 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS PACKAGE. 46 | 47 | *********************** 48 | -------------------------------------------------------------------------------- /ai.py: -------------------------------------------------------------------------------- 1 | """ 2 | ai.py - Artificial Intelligence Module 3 | Copyright 2009-2011, Michael Yanovich, yanovich.net 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.chat 7 | """ 8 | from sopel.module import rule, priority, rate 9 | import random 10 | import time 11 | 12 | 13 | def setup(bot): 14 | # Set value to 3 if not configured 15 | if bot.config.ai and bot.config.ai.frequency: 16 | bot.memory['frequency'] = bot.config.ai.frequency 17 | else: 18 | bot.memory['frequency'] = 3 19 | 20 | random.seed() 21 | 22 | 23 | def decide(bot): 24 | return 0 < random.random() < float(bot.memory['frequency']) / 10 25 | 26 | 27 | @rule('(?i)$nickname\:\s+(bye|goodbye|gtg|seeya|cya|ttyl|g2g|gnight|goodnight)') 28 | @rate(30) 29 | def goodbye(bot, trigger): 30 | byemsg = random.choice(('Bye', 'Goodbye', 'Seeya', 'Auf Wiedersehen', 'Au revoir', 'Ttyl')) 31 | punctuation = random.choice(('!', ' ')) 32 | bot.say(byemsg + ' ' + trigger.nick + punctuation) 33 | 34 | 35 | @rule('(?i).*(thank).*(you).*(sopel|$nickname).*$') 36 | @rate(30) 37 | @priority('high') 38 | def ty(bot, trigger): 39 | human = random.uniform(0, 9) 40 | time.sleep(human) 41 | mystr = trigger.group() 42 | mystr = str(mystr) 43 | if (mystr.find(" no ") == -1) and (mystr.find("no ") == -1) and (mystr.find(" no") == -1): 44 | bot.reply("You're welcome.") 45 | 46 | 47 | @rule('(?i)$nickname\:\s+(thank).*(you).*') 48 | @rate(30) 49 | def ty2(bot, trigger): 50 | ty(bot, trigger) 51 | 52 | 53 | @rule('(?i).*(thanks).*(sopel|$nickname).*') 54 | @rate(40) 55 | def ty4(bot, trigger): 56 | ty(bot, trigger) 57 | 58 | 59 | @rule('(sopel|$nickname)\:\s+(yes|no)$') 60 | @rate(15) 61 | def yesno(bot, trigger): 62 | rand = random.uniform(0, 5) 63 | text = trigger.group() 64 | text = text.split(":") 65 | text = text[1].split() 66 | time.sleep(rand) 67 | if text[0] == 'yes': 68 | bot.reply("no") 69 | elif text[0] == 'no': 70 | bot.reply("yes") 71 | 72 | 73 | @rule('(?i)($nickname|sopel)\:\s+(ping)\s*') 74 | @rate(30) 75 | def ping_reply(bot, trigger): 76 | text = trigger.group().split(":") 77 | text = text[1].split() 78 | if text[0] == 'PING' or text[0] == 'ping': 79 | bot.reply("PONG") 80 | 81 | 82 | @rule('(?i)((sopel|$nickname)\[,:]\s*i.*love|i.*love.*(sopel|$nickname).*)') 83 | @rate(30) 84 | def love(bot, trigger): 85 | bot.reply("I love you too.") 86 | 87 | 88 | @rule('\s*([Xx]+[dD]+|([Hh]+[Aa]+)+)') 89 | @rate(30) 90 | def xd(bot, trigger): 91 | respond = ['xDDDDD', 'XD', 'XDDDD', 'haha'] 92 | randtime = random.uniform(0, 3) 93 | time.sleep(randtime) 94 | bot.say(random.choice(respond)) 95 | 96 | 97 | @rule('(haha!?|lol!?)$') 98 | @priority('high') 99 | def f_lol(bot, trigger): 100 | if decide(bot): 101 | respond = ['haha', 'lol', 'rofl', 'hm', 'hmmmm...'] 102 | randtime = random.uniform(0, 9) 103 | time.sleep(randtime) 104 | bot.say(random.choice(respond)) 105 | 106 | 107 | @rule('^\s*(([Bb]+([Yy]+[Ee]+(\s*[Bb]+[Yy]+[Ee]+)?)|[Ss]+[Ee]{2,}\s*[Yy]+[Aa]+|[Oo]+[Uu]+)|cya|ttyl|[Gg](2[Gg]|[Tt][Gg]|([Oo]{2,}[Dd]+\s*([Bb]+[Yy]+[Ee]+|[Nn]+[Ii]+[Gg]+[Hh]+[Tt]+)))\s*(!|~|.)*)$') 108 | @priority('high') 109 | def f_bye(bot, trigger): 110 | set1 = ['bye', 'byebye', 'see you', 'see ya', 'Good bye', 'have a nice day'] 111 | set2 = ['~', '~~~', '!', ' :)', ':D', '(Y)', '(y)', ':P', ':-D', ';)', '(wave)', '(flee)'] 112 | respond = [ str1 + ' ' + str2 for str1 in set1 for str2 in set2] 113 | bot.say(random.choice(respond)) 114 | 115 | @rule('^\s*(([Hh]+([AaEe]+[Ll]+[Oo]+|[Ii]+)+\s*(all)?)|[Yy]+[Oo]+|[Aa]+[Ll]+|[Aa]nybody)\s*(!+|\?+|~+|.+|[:;][)DPp]+)*$') 116 | @priority('high') 117 | def f_hello(bot, trigger): 118 | randtime = random.uniform(0, 7) 119 | time.sleep(randtime) 120 | set1 = ['yo', 'hey', 'hi', 'Hi', 'hello', 'Hello', 'Welcome'] 121 | set2 = ['~', '~~~', '!', '?', ' :)', ':D', 'xD', '(Y)', '(y)', ':P', ':-D', ';)', ', How do you do?'] 122 | respond = [ str1 + ' ' + str2 for str1 in set1 for str2 in set2] 123 | bot.say(random.choice(respond)) 124 | 125 | 126 | @rule('(heh!?)$') 127 | @priority('high') 128 | def f_heh(bot, trigger): 129 | if decide(bot): 130 | respond = ['hm', 'hmmmmmm...', 'heh?'] 131 | randtime = random.uniform(0, 7) 132 | time.sleep(randtime) 133 | bot.say(random.choice(respond)) 134 | 135 | 136 | @rule('(?i)$nickname\:\s+(really!?)') 137 | @priority('high') 138 | def f_really(bot, trigger): 139 | randtime = random.uniform(10, 45) 140 | time.sleep(randtime) 141 | bot.say(str(trigger.nick) + ": " + "Yes, really.") 142 | 143 | 144 | @rule('^\s*[Ww]([Bb]|elcome\s*back)[\s:,].*$nickname') 145 | def wb(bot, trigger): 146 | set1 = ['Thank you', 'thanks'] 147 | set2 = ['!', ' :)', ' :D'] 148 | respond = [ str1 + str2 for str1 in set1 for str2 in set2] 149 | randtime = random.uniform(0, 7) 150 | time.sleep(randtime) 151 | bot.reply(random.choice(respond)) 152 | 153 | 154 | if __name__ == '__main__': 155 | print(__doc__.strip()) 156 | -------------------------------------------------------------------------------- /bomb.py: -------------------------------------------------------------------------------- 1 | """ 2 | bomb.py - Simple Sopel bomb prank game 3 | Copyright 2012, Edward Powell http://embolalia.net 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.dfbta.net 7 | """ 8 | from sopel.module import commands 9 | from random import choice, randint 10 | from re import search 11 | import sched 12 | import time 13 | 14 | colors = ['Red', 'Yellow', 'Blue', 'White', 'Black'] 15 | sch = sched.scheduler(time.time, time.sleep) 16 | fuse = 120 # seconds 17 | bombs = dict() 18 | 19 | 20 | @commands('bomb') 21 | def start(bot, trigger): 22 | """ 23 | Put a bomb in the specified user's pants. They will be kicked if they 24 | don't guess the right wire fast enough. 25 | """ 26 | if not trigger.group(2): 27 | return 28 | 29 | if not trigger.sender.startswith('#') or \ 30 | (trigger.nick not in bot.ops[trigger.sender] and 31 | trigger.nick not in bot.halfplus[trigger.sender]): 32 | return 33 | global bombs 34 | global sch 35 | target = trigger.group(2).split(' ')[0] 36 | if target in bot.config.other_bots or target == bot.nick: 37 | return 38 | if target in bombs: 39 | bot.say('I can\'t fit another bomb in ' + target + '\'s pants!') 40 | return 41 | message = 'Hey, ' + target + '! Don\'t look but, I think there\'s a bomb in your pants. 2 minute timer, 5 wires: Red, Yellow, Blue, White and Black. Which wire should I cut? Don\'t worry, I know what I\'m doing! (respond with .cutwire color)' 42 | bot.say(message) 43 | color = choice(colors) 44 | bot.msg(trigger.nick, 45 | "Hey, don\'t tell %s, but the %s wire? Yeah, that\'s the one." 46 | "But shh! Don\'t say anything!" % (target, color)) 47 | code = sch.enter(fuse, 1, explode, (bot, trigger)) 48 | bombs[target.lower()] = (color, code) 49 | sch.run() 50 | 51 | 52 | @commands('cutwire') 53 | def cutwire(bot, trigger): 54 | """ 55 | Tells sopel to cut a wire when you've been bombed. 56 | """ 57 | global bombs, colors 58 | target = trigger.nick 59 | if target.lower() != bot.nick.lower() and target.lower() not in bombs: 60 | return 61 | color, code = bombs.pop(target.lower()) # remove target from bomb list 62 | wirecut = trigger.group(2).rstrip(' ') 63 | if wirecut.lower() in ('all', 'all!'): 64 | sch.cancel(code) # defuse timer, execute premature detonation 65 | kmsg = ('KICK %s %s : Cutting ALL the wires! *boom* (You should\'ve picked the %s wire.)' 66 | % (trigger.sender, target, color)) 67 | bot.write([kmsg]) 68 | elif wirecut.capitalize() not in colors: 69 | bot.say('I can\'t seem to find that wire, ' + target + '! You sure you\'re picking the right one? It\'s not here!') 70 | bombs[target.lower()] = (color, code) # Add the target back onto the bomb list, 71 | elif wirecut.capitalize() == color: 72 | bot.say('You did it, ' + target + '! I\'ll be honest, I thought you were dead. But nope, you did it. You picked the right one. Well done.') 73 | sch.cancel(code) # defuse bomb 74 | else: 75 | sch.cancel(code) # defuse timer, execute premature detonation 76 | kmsg = 'KICK ' + trigger.sender + ' ' + target + \ 77 | ' : No! No, that\'s the wrong one. Aww, you\'ve gone and killed yourself. Oh, that\'s... that\'s not good. No good at all, really. Wow. Sorry. (You should\'ve picked the ' + color + ' wire.)' 78 | bot.write([kmsg]) 79 | 80 | 81 | def explode(bot, trigger): 82 | target = trigger.group(1) 83 | kmsg = 'KICK ' + trigger.sender + ' ' + target + \ 84 | ' : Oh, come on, ' + target + '! You could\'ve at least picked one! Now you\'re dead. Guts, all over the place. You see that? Guts, all over YourPants. (You should\'ve picked the ' + bombs[target.lower()][0] + ' wire.)' 85 | bot.write([kmsg]) 86 | bombs.pop(target.lower()) 87 | -------------------------------------------------------------------------------- /calibre.py: -------------------------------------------------------------------------------- 1 | """ 2 | Name: Calibre Search 3 | Purpose: Allows your IRC bot (Sopel) to search a configured Calibre library 4 | Author: Kevin Laurier 5 | Created: 14/11/2013 6 | Copyright: (c) Kevin Laurier 2013 7 | Licence: GPLv3 8 | 9 | This module allows your Sopel bot to act as an interface for a configured Calibre 10 | server by using its REST API. You can enter search words to obtain a list of URLs 11 | to the ebooks stored on the server. The Calibre server may be remote or on the 12 | local machine. 13 | """ 14 | 15 | 16 | from base64 import b64encode 17 | import requests 18 | from sopel.module import commands, example 19 | 20 | 21 | class CalibreRestFacade(object): 22 | """ 23 | Connect to Calibre using its REST api 24 | """ 25 | def __init__(self, url, username, password): 26 | """ 27 | Initialize a connection to Calibre using the Requests library 28 | """ 29 | self.url = url 30 | self.username = username 31 | self.password = password 32 | self.auth = requests.auth.HTTPDigestAuth(username, password) 33 | 34 | def books(self, book_ids): 35 | """ 36 | Get all books corresponding to a list of IDs 37 | """ 38 | book_ids_csv = ','.join(str(b_id) for b_id in book_ids) 39 | return requests.get(self.url + '/ajax/books', 40 | auth=self.auth, params={'ids': book_ids_csv}).json() 41 | 42 | def search(self, keywords): 43 | """ 44 | Get a list of IDs corresponding to the search results 45 | """ 46 | return requests.get(self.url + '/ajax/search', 47 | auth=self.auth, params={'query': keywords}).json() 48 | 49 | 50 | def configure(config): 51 | """ 52 | | [calibre] | example | purpose | 53 | | ---------- | ------- | ------- | 54 | | url | http://localhost:8080 | The URL to your Calibre server | 55 | | username | calibre | The username used to log on your calibre server (if any) | 56 | | password | password | The password used to log on your calibre server (if any) | 57 | """ 58 | if config.option('Configure calibre module', False): 59 | if not config.has_section('calibre'): 60 | config.add_section('calibre') 61 | config.interactive_add('calibre', 'url', "Enter the URL to your Calibre server (without trailing slashes)") 62 | 63 | if config.option('Configure username / password for your Calibre server?'): 64 | config.interactive_add('calibre', 'username', "Enter your Calibre username") 65 | config.interactive_add('calibre', 'password', "Enter your Calibre password", ispass=True) 66 | config.save() 67 | 68 | 69 | def setup(bot): 70 | c = bot.config.calibre 71 | bot.memory['calibre'] = CalibreRestFacade(c.url, c.username, c.password) 72 | 73 | 74 | @commands('calibre', 'cal') 75 | @example('.calibre gods of eden') 76 | @example('.calibre') 77 | def calibre(bot, trigger): 78 | """ 79 | Queries a configured Calibre library and returns one or more URLs 80 | corresponding to the search results. If no search words are entered, 81 | the URL of the Calibre server will be returned. 82 | """ 83 | search_words = trigger.group(2) 84 | if not search_words: 85 | bot.reply('The Calibre library is here: ' + bot.config.calibre.url) 86 | return 87 | 88 | calibre = bot.memory['calibre'] 89 | book_ids = calibre.search(search_words)['book_ids'] 90 | num_books = len(book_ids) 91 | 92 | if num_books == 1: 93 | book_title = calibre.books(book_ids).values()[0]['title'] 94 | bot.reply(u'{}: {}/browse/book/{}' 95 | .format(book_title, calibre.url, book_ids[0])) 96 | 97 | elif num_books > 1: 98 | results = calibre.books(book_ids) 99 | books = [(book_id, results[str(book_id)]) for book_id in book_ids] 100 | 101 | bot.reply("I'm sending you a private message of all Alexandria search results!") 102 | bot.msg(trigger.nick, "{} results for '{}'" 103 | .format(len(books), search_words)) 104 | 105 | for book_id, book in books: 106 | bot.msg(trigger.nick, u'{}: {}/browse/book/{}' 107 | .format(book['title'], calibre.url, book_id)) 108 | else: 109 | bot.say("Calibre: No results found.") 110 | 111 | 112 | @commands('calurl') 113 | def calinfo(bot, trigger): 114 | """ 115 | Displays the URL of your configured Calibre server 116 | """ 117 | bot.say('URL: ' + bot.config.calibre.url) 118 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | debug.py - Sopel Debugging Module 4 | Copyright 2013, Dimitri "Tyrope" Molenaars, Tyrope.nl 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://sopel.chat 8 | """ 9 | 10 | from sopel.module import commands, example 11 | 12 | @commands('privs') 13 | @example('.privs', '.privs #channel') 14 | def privileges(bot, trigger): 15 | """Print the privileges of a specific channel, or the entire array.""" 16 | if trigger.group(2): 17 | try: 18 | bot.say(str(bot.privileges[trigger.group(2)])) 19 | except Exception: 20 | bot.say("Channel not found.") 21 | else: 22 | bot.say(str(bot.privileges)) 23 | 24 | @commands('admins') 25 | @example('.admins') 26 | def admins(bot, trigger): 27 | """Print the list of admins, including the owner.""" 28 | owner = bot.config.core.owner 29 | admins = str(bot.config.core.get_list('admins')) 30 | bot.say("[Owner]"+owner+" [Admins]"+admins) 31 | 32 | @commands('debug_print') 33 | @example('.debug_print') 34 | def debug_print(bot, trigger): 35 | """Calls version, admins and privileges prints in sequence.""" 36 | try: 37 | sopel.modules.version.version(bot, trigger) 38 | except Exception as e: 39 | bot.say('An error occurred trying to get the current version.') 40 | admins(bot, trigger) 41 | privileges(bot, trigger) 42 | 43 | @commands('raiseException', 'causeProblems', 'giveError') 44 | @example('.raiseException') 45 | def cause_problems(bot, trigger): 46 | """This deliberately causes sopel to raise exceptional problems.""" 47 | raise Exception("Problems were caused on command.") 48 | 49 | -------------------------------------------------------------------------------- /dicelog.py: -------------------------------------------------------------------------------- 1 | """ 2 | dice.py - Dice Module 3 | Copyright 2010-2013, Dimitri "Tyrope" Molenaars, TyRope.nl 4 | Copyright 2013 , Lior "Eyore" Ramati , FireRogue517@gmail.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://sopel.chat/ 8 | """ 9 | 10 | from __future__ import print_function 11 | from sopel.module import commands, priority 12 | from random import randint, seed 13 | import time 14 | import os.path 15 | from sopel.modules.calc import calculate 16 | import re 17 | 18 | seed() 19 | 20 | 21 | def setup(bot): 22 | if not bot.config.has_section('dicelog'): 23 | bot.config.add_section('dicelog') 24 | if not bot.config.has_option('dicelog', 'logdir'): 25 | bot.config.parser.set('dicelog', 'logdir', '.') 26 | if not bot.config.has_option('dicelog', 'campaigns'): 27 | bot.config.parser.set('dicelog', 'campaigns', '') 28 | 29 | 30 | def configure(config): 31 | """ Since this module conflicts with the default dice module, this module 32 | will ask the user to blacklist one or the other. If dicelog is kept, it 33 | asks for a directory to store the logs, and also for a list of campaigns 34 | to recognize. 35 | """ 36 | which = config.option("This module conflicts with the default dice module. Should I disable it and to allow this one to run", True) 37 | module = "dice" if which else "dicelog" 38 | print("The %s module is being added to the module blacklist." % module) 39 | if config.has_option('core', 'exclude'): 40 | if module not in config.core.exclude: 41 | config.core.exclude = ','.join([config.core.exclude, ' module']) 42 | else: 43 | if not config.has_option('core', 'enable'): 44 | config.parser.set('core', 'exclude', module) 45 | if module == "dicelog": 46 | return 47 | config.interactive_add('dicelog', 'logdir', 48 | "where should the log files be stored on the harddrive?") 49 | config.add_list('dicelog', 'campaigns', "\ 50 | You may now type out any campaigns you wish to include by default. Commands are\ 51 | provided to edit this list on the fly. Please be aware that this identifier is\ 52 | what will be used to log rolls and also name the files.", "Campaign Identifier:") 53 | config.dicelog.campaigns = config.dicelog.campaigns.lower() 54 | 55 | 56 | @commands('d', 'dice', 'roll') 57 | @priority('medium') 58 | def dicelog(bot, trigger): 59 | """ 60 | .dice [logfile] - Rolls dice using the XdY format, also does 61 | basic math and drop lowest (XdYvZ). Saves result in logfile if given. 62 | """ 63 | if not trigger.group(2): 64 | return bot.reply('You have to specify the dice you wanna roll.') 65 | 66 | # extract campaign 67 | if trigger.group(2).startswith('['): 68 | campaign, rollStr = trigger.group(2)[1:].split(']') 69 | else: 70 | campaign = '' 71 | rollStr = trigger.group(2).strip() 72 | campaign = campaign.strip() 73 | rollStr = rollStr.strip() 74 | # prepare string for mathing 75 | arr = rollStr.lower().replace(' ', '') 76 | arr = arr.replace('-', ' - ').replace('+', ' + ').replace('/', ' / ') 77 | arr = arr.replace('*', ' * ').replace('(', ' ( ').replace(')', ' ) ') 78 | arr = arr.replace('^', ' ^ ').replace('()', '').split(' ') 79 | full_string, calc_string = '', '' 80 | 81 | for segment in arr: 82 | # check for dice 83 | result = re.search("([0-9]+m)?([0-9]*d[0-9]+)(v[0-9]+)?", segment) 84 | if result: 85 | # detect droplowest 86 | if result.group(3) is not None: 87 | #check for invalid droplowest 88 | dropLowest = int(result.group(3)[1:]) 89 | # or makes implied 1dx to be evaluated in case of dx being typed 90 | if (dropLowest >= int(result.group(2).split('d')[0] or 1)): 91 | bot.reply('You\'re trying to drop too many dice.') 92 | return 93 | else: 94 | dropLowest = 0 95 | 96 | # on to rolling dice! 97 | value, drops = '(', '' 98 | # roll... 99 | dice = rollDice(result.group(2)) 100 | for i in range(0, len(dice)): 101 | # format output 102 | if i < dropLowest: 103 | if drops == '': 104 | drops = '[+' 105 | drops += str(dice[i]) 106 | if i < dropLowest - 1: 107 | drops += '+' 108 | else: 109 | drops += ']' 110 | else: 111 | value += str(dice[i]) 112 | if i != len(dice) - 1: 113 | value += '+' 114 | no_dice = False 115 | value += drops + ')' 116 | else: 117 | value = segment 118 | full_string += value 119 | # and repeat 120 | 121 | # replace, split and join to exclude dropped dice from the math. 122 | result = calculate(''.join( 123 | full_string.replace('[', '#').replace(']', '#').split('#')[::2])) 124 | if result == 'Sorry, no result.': 125 | bot.reply('Calculation failed, did you try something weird?') 126 | elif(no_dice): 127 | bot.reply('For pure math, you can use .c ' 128 | + rollStr + ' = ' + result) 129 | else: 130 | response = 'You roll ' + rollStr + ': ' + full_string + ' = ' + result 131 | bot.reply(response) 132 | campaign = campaign.strip().lower() 133 | if campaign: 134 | if campaign in bot.config.dicelog.campaigns.split(','): 135 | log = open(os.path.join(bot.config.dicelog.logdir, campaign + '.log'), 'a') 136 | log.write("At <%s> %s rolled %s\n" % (time.ctime(), trigger.nick, response[9:])) 137 | log.close() 138 | else: 139 | bot.reply("Didn't log because " + campaign + " is not listed as a campaign. sorry!") 140 | 141 | 142 | def rollDice(diceroll): 143 | rolls = int(diceroll.split('d')[0] or 1) 144 | size = int(diceroll.split('d')[1]) 145 | result = [] # dice results. 146 | 147 | for i in range(1, rolls + 1): 148 | #roll 10 dice, pick a random dice to use, add string to result. 149 | result.append((randint(1, size), randint(1, size), randint(1, size), 150 | randint(1, size), randint(1, size), randint(1, size), 151 | randint(1, size), randint(1, size), randint(1, size), 152 | randint(1, size))[randint(0, 9)]) 153 | return sorted(result) # returns a set of integers. 154 | 155 | 156 | @commands('campaign', 'campaigns') 157 | @priority('medium') 158 | def campaign(bot, trigger): 159 | if trigger.group(2): 160 | command, campaign = trigger.group(2).partition(' ')[::2] 161 | else: 162 | return bot.say('usage: campaign (list|add|del) ') 163 | if not command in ['list', 'add', 'del']: 164 | return bot.say('usage: campaign (list|add|del) ') 165 | if not command == 'list': 166 | if not trigger.admin: 167 | return 168 | elif not campaign: 169 | return bot.say('usage: campaign (list|add|del) ') 170 | campaign = campaign.lower().strip() 171 | campaigns = bot.config.dicelog.campaigns.split(', ') 172 | if campaign in campaigns: 173 | if command == 'del': 174 | campaigns.remove(campaign) 175 | bot.say("Campaign \"%s\" has been removed!" % campaign) 176 | else: # command == 'add' 177 | bot.say("Campaign \"%s\" already exists!" % campaign) 178 | else: 179 | if command == 'del': 180 | bot.say("Campaign \"%s\" doesn't exist!" % campaign) 181 | else: # command == 'add' 182 | campaigns.append(campaign) 183 | if not command == 'list': 184 | bot.config.dicelog.campaigns = ', '.join(campaigns) 185 | bot.say("The current list is: " + bot.config.dicelog.campaigns) 186 | 187 | if __name__ == '__main__': 188 | print(__doc__.strip()) 189 | -------------------------------------------------------------------------------- /document.py: -------------------------------------------------------------------------------- 1 | from sopel import coretasks 2 | from sopel.module import commands 3 | import os 4 | import subprocess 5 | 6 | 7 | def configure(config): 8 | """ 9 | | [document] | example | purpose | 10 | | ---------- | ------- | ------- | 11 | | layout | default | The jekyll layout to use for the commands page | 12 | | base_dir | /home/user/sopel-website | The location of the jekyll site source | 13 | | plugins_dir | /home/user/sopel-website/_plugins | The location of the jekyll plugins directory | 14 | | layouts_dir | /home/user/sopel-website/_layouts | The location of the jekyll layouts directory | 15 | | output_dir | /var/www/sopel | The location of the jekyll generated site | 16 | | jekyll_location | /opt/jekyll/jekyll | The location of the jekyll executable. Not needed if jekyll is on the user's PATH | 17 | """ 18 | if config.option('Compile command listings to a Jekyll site', False): 19 | config.add_section('document') 20 | config.interactive_add('document', 'layout', 21 | 'The jekyll layout to use for the commands page') 22 | config.interactive_add('document', 'base_dir', 23 | 'The location of the jekyll site source ') 24 | config.interactive_add('document', 'plugins_dir', 25 | 'The location of the jekyll plugins directory') 26 | config.interactive_add('document', 'layouts_dir', 27 | 'The location of the jekyll layouts directory') 28 | config.interactive_add('document', 'output_dir', 29 | 'The location of the jekyll generated site') 30 | config.interactive_add('document', 'jekyll_location', 31 | "The location of the jekyll executable. Not needed if jekyll is on" 32 | " the user's PATH.") 33 | 34 | 35 | def setup(bot): 36 | if not bot.config.document.base_dir: 37 | raise ConfigurationError('Must provide Jekyll base_dir') 38 | 39 | 40 | @commands('document') 41 | def document(bot, trigger): 42 | conf = bot.config.document 43 | layout = conf.layout or 'default' 44 | base_dir = conf.base_dir 45 | output_dir = conf.output_dir or os.path.join(base_dir, '_site') 46 | 47 | with open(os.path.join(base_dir, 'modules.md'), 'w') as f: 48 | front_matter = '''---\nlayout: {}\ntitle: {} commands list\n---\n\n''' 49 | f.write(front_matter.format(layout, bot.nick)) 50 | f.write('| Command | Purpose | Example |\n') 51 | f.write('| ------- | ------- | ------- |\n') 52 | 53 | for command in sorted(bot.doc.iterkeys()): 54 | doc = bot.doc[command] 55 | docstring = doc[0].replace('\n\n', '
').replace('\n', ' ') 56 | f.write('| {} | {} | {} |\n'.format(command, docstring, doc[1])) 57 | command = "{} build -s {} -d {}" 58 | command = command.format(conf.jekyll_location or 'jekyll', base_dir, 59 | output_dir) 60 | # We don't give a shit what it says, but it fucking crashes if we don't 61 | # listen. Fucking needy asshole piece of Ruby shit. 62 | data = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, 63 | stderr=subprocess.PIPE).communicate() 64 | bot.say('Finished processing documentation.') 65 | -------------------------------------------------------------------------------- /fuckingweather.py: -------------------------------------------------------------------------------- 1 | """ 2 | fuckingweather.py - Sopel module for The Fucking Weather 3 | Copyright 2013 Michael Yanovich 4 | Copyright 2013 Edward Powell 5 | 6 | Licensed under the Eiffel Forum License 2. 7 | 8 | http://sopel.chat 9 | """ 10 | from sopel.module import commands, rate, priority, NOLIMIT 11 | from sopel import web 12 | import re 13 | 14 | 15 | @commands('fucking_weather', 'fw') 16 | @rate(30) 17 | @priority('low') 18 | def fucking_weather(bot, trigger): 19 | text = trigger.group(2) 20 | if not text: 21 | bot.reply("INVALID FUCKING PLACE. PLEASE ENTER A FUCKING ZIP CODE, OR A FUCKING CITY-STATE PAIR.") 22 | return 23 | text = web.quote(text) 24 | page = web.get("http://thefuckingweather.com/Where/%s" % (text)) 25 | re_mark = re.compile('

(.*?)

') 26 | results = re_mark.findall(page) 27 | if results: 28 | bot.reply(results[0]) 29 | else: 30 | bot.reply("I CAN'T GET THE FUCKING WEATHER.") 31 | return bot.NOLIMIT 32 | -------------------------------------------------------------------------------- /helpbot.py: -------------------------------------------------------------------------------- 1 | """ 2 | help.py - HelpBot Module 3 | Copyright 2013, Dimitri "Tyrope" Molenaars, TyRope.nl 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.chat/ 7 | """ 8 | from __future__ import print_function 9 | from sopel.module import rule, event, commands 10 | from collections import deque 11 | 12 | helpees = deque() 13 | 14 | def configure(config): 15 | """ 16 | To use the helpbot module, you have to set your help channel. 17 | 18 | | [helpbot] | example | purpose | 19 | | -------- | ------- | ------- | 20 | | channel | #help | Enter the channel HelpBot should moderate | 21 | """ 22 | config.interactive_add('helpbot', 'channel', "Enter the channel HelpBot should moderate", None) 23 | 24 | def setup(bot): 25 | if not bot.config.helpbot.channel: 26 | raise ConfigurationError('Helpbot module not configured') 27 | 28 | @event('JOIN') 29 | @rule(r'.*') 30 | def addNewHelpee(bot, trigger): 31 | """Adds somebody who joins the channel to the helpee list.""" 32 | if trigger.admin or trigger.nick == bot.nick or trigger.sender != bot.config.helpbot.channel: 33 | return 34 | if trigger.isop: 35 | sopel.say('An operator has joined the help channel: ' + trigger.nick) 36 | return 37 | helpees.append({'nick': trigger.nick, 'request': None, 'active': False, 'skipped': False}) 38 | try: 39 | bot.reply('Welcome to '+trigger.sender+'. Please PM '+bot.nick+' with your help request, prefixed with \'.request\' (Example: /msg '+bot.nick+' .request I lost my password.) Don\'t include any private information with your question (passwords etc), as the question will be posted in this channel') 40 | except AttributeError: 41 | bot.debug('Help','You\'re running a module requiring configuration, without having configured it.','warning') 42 | return 43 | 44 | @event('NICK') 45 | @rule(r'.*') 46 | def helpeeRename(bot, trigger): 47 | """ Update the list when somebody changes nickname. """ 48 | for h in helpees: 49 | if h['nick'] == trigger.nick: 50 | h['nick'] = trigger.args[0] 51 | return 52 | 53 | @event('QUIT') 54 | @rule(r'.*') 55 | def helpeeQuit(bot, trigger): 56 | """Dispatch for removing somebody from the helpee list on-quit.""" 57 | removeHelpee(bot, trigger) 58 | 59 | @event('PART') 60 | @rule(r'.*') 61 | def helpeePart(bot, trigger): 62 | """Dispatch for removing somebode from the helpee list when they leave the channel.""" 63 | if trigger.sender != bot.config.helpbot.channel: 64 | return 65 | else: 66 | removeHelpee(bot, trigger) 67 | 68 | def removeHelpee(bot, trigger): 69 | """Removes somebody from the helpee list.""" 70 | for i in range(len(helpees)): 71 | if trigger.nick == helpees[i]['nick']: 72 | try: 73 | helpees.remove(helpees[i]) 74 | return bot.msg(bot.config.helpbot.channel, trigger.nick+' removed from waiting list.') 75 | except ValueError as e: 76 | bot.debug('Help', str(e), 'warning') 77 | return bot.msg(bot.config.helpbot.channel, 'Error removing %s from helpees list.' % (trigger.nick,)) 78 | 79 | @commands('request') 80 | def request(bot, trigger): 81 | """Allows a helpee to add a message to their help request, and activates said request.""" 82 | if trigger.sender.startswith("#"): return 83 | found = None 84 | for helpee in helpees: 85 | if trigger.nick == helpee['nick']: 86 | found = helpee 87 | if not found: 88 | return bot.say('You\'re not found in the list of people in the channel, are you sure you\'re in the channel?') 89 | else: 90 | if not trigger.groups()[1]: 91 | return bot.say('You forgot to actually state your question, example: .request Who the eff is Hank?') 92 | if not helpee['active']: 93 | helpee['active'] = True 94 | helpee['request'] = trigger.groups()[1].encode('UTF-8') 95 | bot.say('Your help request is now marked active. Your question is:') 96 | bot.say(helpee['request']) 97 | bot.say('If you have anything more to add, please use the .request command again. Please note that when you leave the channel your request will be deleted.') 98 | bot.msg(bot.config.helpbot.channel,trigger.nick+' just added a question to their help request.') 99 | else: 100 | helpee['request'] += ' '+trigger.groups()[1].encode('UTF-8') 101 | bot.say('You already had a question, I\'ve added this to what you\'ve asked previously. Your new question is:') 102 | bot.say(helpee['request']) 103 | 104 | @commands('next') 105 | def next(bot, trigger): 106 | """Allows a channel operator to get the next person in the waiting list, if said person didn't activate his or her help request, it reminds them and puts them at the end of the queue.""" 107 | if not trigger.isop: return bot.reply('You\'re not a channel operator.') 108 | try: 109 | helpee = helpees.popleft() 110 | except: 111 | return bot.reply('Nobody waiting.') 112 | if not helpee['active']: 113 | if not helpee['skipped']: 114 | helpee['skipped'] = True 115 | helpees.append(helpee) 116 | bot.reply('Tried assigning '+helpee['nick']+' but they didn\'t set a help request, if they\'re the only person waiting for help please give them some time to set one.') 117 | bot.msg(helpee['nick'], 'An operator just tried to help you but you didn\'t tell me what you need help with. I\'m putting you back at the end of the queue, please use the .request command.') 118 | else: 119 | bot.msg(helpee['nick'], 'An operator just tried to help you again. Since you seem to be inactive I\'m going to remove you from the channel. Please use the .request command after you join again.') 120 | bot.write(['KICK', bot.config.helpbot.channel, helpee['nick'], 'Didn\'t set a help request before being assigned an operator, twice.']) 121 | bot.reply('Attempted to kick '+helpee['nick']+', due to being called twice without sending a request.') 122 | else: 123 | bot.reply('assigned '+helpee['nick']+' to you. Their question: '+helpee['request']) 124 | bot.write(['MODE', bot.config.helpbot.channel, '+v', helpee['nick']]) 125 | 126 | if __name__ == '__main__': 127 | print(__doc__.strip()) 128 | -------------------------------------------------------------------------------- /imgur.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | imgur.py - Sopel imgur Information Module 4 | Copyright © 2014, iceTwy, 5 | Licensed under the Eiffel Forum License 2. 6 | """ 7 | 8 | import json 9 | import re 10 | import os.path 11 | import sys 12 | if sys.version_info.major < 3: 13 | from urllib2 import HTTPError 14 | from urlparse import urlparse 15 | else: 16 | from urllib.request import HTTPError 17 | from urllib.parse import urlparse 18 | from sopel.config import ConfigurationError 19 | from sopel import web, tools 20 | from sopel.module import rule 21 | 22 | class ImgurClient(object): 23 | def __init__(self, client_id): 24 | """ 25 | Sets the client_id (obtain yours here: https://api.imgur.com/oauth2/addclient) 26 | and the imgur API URL. 27 | """ 28 | self.client_id = client_id 29 | self.api_url = "https://api.imgur.com/3/" 30 | 31 | def request(self, input): 32 | """ 33 | Sends a request to the API. Only publicly available data is accessible. 34 | Returns data as JSON. 35 | """ 36 | headers = {'Authorization': 'Client-ID ' + self.client_id, 37 | 'Accept': 'application/json'} 38 | request = web.get(self.api_url + input, headers=headers) 39 | #FIXME: raise for status 40 | return json.loads(request) 41 | 42 | def resource(self, resource, id): 43 | """ 44 | Retrieves a resource from the imgur API. 45 | Returns data as JSON. 46 | """ 47 | api_request_path = '{0}/{1}'.format(resource, id) 48 | return self.request(api_request_path) 49 | 50 | def configure(config): 51 | """ 52 | The client ID can be obtained by registering your bot at 53 | https://api.imgur.com/oauth2/addclient 54 | 55 | | [imgur] | example | purpose | 56 | | --------- | --------------- | -------------------------------- | 57 | | client_id | 1b3cfe15768ba29 | Bot's ID, for Imgur's reference. | 58 | """ 59 | 60 | if config.option('Configure Imgur? (You will need to register at https://api.imgur.com/oauth2/addclient)', False): 61 | config.interactive_add('imgur', 'client_id', 'Client ID') 62 | 63 | def setup(bot): 64 | """ 65 | Tests the validity of the client ID given in the configuration. 66 | If it is not, initializes sopel's memory callbacks for imgur URLs, 67 | and uses them as the trigger for the link parsing function. 68 | """ 69 | try: 70 | client = ImgurClient(bot.config.imgur.client_id) 71 | client.request('gallery.json') 72 | except HTTPError: 73 | raise ConfigurationError('Could not validate the client ID with Imgur. \ 74 | Are you sure you set it up correctly?') 75 | imgur_regex = re.compile('(?:https?://)?(?:i\.)?imgur\.com/(.*)$') 76 | if not bot.memory.contains('url_callbacks'): 77 | bot.memory['url_callbacks'] = tools.SopelMemory() 78 | bot.memory['url_callbacks'][imgur_regex] = imgur 79 | 80 | def album(link_id, bot): 81 | """ 82 | Handles information retrieval for non-gallery albums. 83 | The bot will output the title, the number of images and the number of views 84 | of the album. 85 | """ 86 | client = ImgurClient(bot.config.imgur.client_id) 87 | api_response = client.resource('album', link_id) 88 | album = api_response['data'] 89 | return bot.say('[imgur] [{0} - an album with {1} images and ' \ 90 | '{2} views]'.format(album['title'].encode('utf-8'), 91 | str(album['images_count']), \ 92 | str(album['views']))) 93 | 94 | def gallery(link_id, bot): 95 | """ 96 | Handles information retrieval for gallery images and albums. 97 | The bot will output the title, the type (image/album/gif), the number of 98 | views, the number of upvotes/downvotes of the gallery resource. 99 | """ 100 | client = ImgurClient(bot.config.imgur.client_id) 101 | api_response = client.resource('gallery', link_id) 102 | gallery = api_response['data'] 103 | if gallery['is_album']: 104 | return bot.say('[imgur] [{0} - a gallery album with {1} views ' \ 105 | '({2} ups and {3} downs)]'.format(gallery['title'].encode('utf-8'), \ 106 | str(gallery['views']), \ 107 | str(gallery['ups']), \ 108 | str(gallery['downs']))) 109 | if gallery['animated'] == True: 110 | return bot.say('[imgur] [{0} - a gallery gif with {1} views ' \ 111 | '({2} ups and {3} downs)]'.format(gallery['title'].encode('utf-8'), \ 112 | str(gallery['views']), \ 113 | str(gallery['ups']), \ 114 | str(gallery['downs']))) 115 | else: 116 | return bot.say('[imgur] [{0} - a gallery image with {1} views ' \ 117 | '({2} ups and {3} downs)]'.format(gallery['title'].encode('utf-8'), \ 118 | str(gallery['views']), 119 | str(gallery['ups']), 120 | str(gallery['downs']))) 121 | 122 | def user(username, bot): 123 | """ 124 | Handles information retrieval for user accounts. 125 | The bot will output the name, and the numbers of submissions, comments and 126 | liked resources, of the selected user. 127 | """ 128 | client = ImgurClient(bot.config.imgur.client_id) 129 | api_response_account = client.resource('account', username) 130 | api_response_gallery_profile = client.resource('account', username + '/gallery_profile') 131 | account = api_response_account['data'] 132 | gallery_profile = api_response_gallery_profile['data'] 133 | return bot.say('[imgur] [{0} is an imgurian with {1} points of reputation, ' \ 134 | '{2} gallery submissions, {3} comments ' \ 135 | 'and {4} likes]'.format(account['url'], \ 136 | str(account['reputation']), \ 137 | str(gallery_profile['total_gallery_submissions']), \ 138 | str(gallery_profile['total_gallery_comments']), \ 139 | str(gallery_profile['total_gallery_likes']))) 140 | 141 | def image(link_id, bot): 142 | """ 143 | Handles information retrieval for non-gallery images. 144 | The bot will output the title, the type (image/gif) and the number of views 145 | of the selected image. 146 | """ 147 | client = ImgurClient(bot.config.imgur.client_id) 148 | api_response = client.resource('image', link_id) 149 | img = api_response['data'] 150 | if img['title']: 151 | title = img['title'] 152 | if not img['title'] and img['description']: 153 | title = img['description'] 154 | if not img['title'] and not img['description']: 155 | title = 'untitled' 156 | if img['animated']: 157 | return bot.say('[imgur] [{0} - a gif with {1} views]'.format(title.encode('utf-8'), \ 158 | str(img['views']))) 159 | else: 160 | return bot.say('[imgur] [{0} - an image with {1} views]'.format(title.encode('utf-8'), \ 161 | str(img['views']))) 162 | 163 | @rule('(?:https?://)?(?:i\.)?imgur\.com/(.*)$') 164 | def imgur(bot, trigger): 165 | """ 166 | Parses the input URL and calls the appropriate function for the resource 167 | (an image or an album). 168 | 169 | imgur has two types of resources: non-gallery and gallery resources. 170 | Non-gallery resources are images and albums that have not been uploaded 171 | to the imgur gallery (imgur.com/gallery), whilst gallery resources have 172 | been. 173 | 174 | * imgur.com/id can refer to two distinct resources (i.e. a non-gallery image 175 | and a gallery resource, e.g. imgur.com/VlmfH and imgur.com/gallery/VlmfH) 176 | 177 | * i.imgur.com/id refers by default to the same non-gallery resource as 178 | imgur.com/id, if there are two distinct resources for this ID. 179 | It refers to the gallery resource if only the gallery resource exists. 180 | 181 | * imgur.com/gallery/id refers solely to a gallery resource. 182 | 183 | * imgur.com/a/id refers solely to an album. Non-gallery data is returned, 184 | even if it is in the gallery. 185 | 186 | * imgur.com/user/username refers solely to an imgur user account. 187 | 188 | The regex rule above will capture either an ID to a gallery or non-gallery 189 | image or album, or a path to a certain imgur resource (e.g. gallery/id, 190 | user/username, and so forth). 191 | 192 | It is more fool-proof to only demand gallery data from the imgur API 193 | if we get a link that is of the form imgur.com/gallery/id, because 194 | imgur IDs are not unique (see above) and we can trigger an error if 195 | we request inexistent gallery data. 196 | """ 197 | 198 | #urlparse does not support URLs without a scheme. 199 | #Add 'https' scheme to an URL if it has no scheme. 200 | if not urlparse(trigger).scheme: 201 | trigger = "https://" + trigger 202 | 203 | """Handle i.imgur.com links first. 204 | They can link to non-gallery images, so we do not request gallery data, 205 | but simply image data.""" 206 | if urlparse(trigger).netloc == 'i.imgur.com': 207 | image_id = os.path.splitext(os.path.basename(urlparse(trigger).path))[0] # get the ID from the img 208 | return image(image_id, bot) 209 | 210 | """Handle imgur.com/* links.""" 211 | #Get the path to the requested resource, from the URL (id, gallery/id, user/username, a/id) 212 | resource_path = urlparse(trigger).path.lstrip('/') 213 | 214 | #The following API endpoints require user authentication, which we do not support. 215 | unauthorized = ['settings', 'notifications', 'message', 'stats'] 216 | if any(item in resource_path for item in unauthorized): 217 | return bot.reply("[imgur] Unauthorized action.") 218 | 219 | #Separate the URL path into an ordered list of the form ['gallery', 'id'] 220 | resource_path_parts = filter(None, resource_path.split('/')) 221 | 222 | #Handle a simple link to imgur.com: no ID is given, meaning that the length of the above list is null 223 | if len(resource_path_parts) == 0: 224 | return 225 | 226 | #Handle a link with a path that has more than two components 227 | if len(resource_path_parts) > 2: 228 | return bot.reply("[imgur] Invalid link.") 229 | 230 | #Handle a link to an ID: imgur.com/id 231 | if len(resource_path_parts) == 1: 232 | return image(resource_path_parts[0], bot) 233 | 234 | #Handle a link to a gallery image/album: imgur.com/gallery/id 235 | if resource_path_parts[0] == 'gallery': 236 | return gallery(resource_path_parts[1], bot) 237 | 238 | #Handle a link to an user account/profile: imgur.com/user/username 239 | if resource_path_parts[0] == 'user': 240 | return user(resource_path_parts[1], bot) 241 | 242 | #Handle a link to an album: imgur.com/a/id 243 | if resource_path_parts[0] == 'a': 244 | return album(resource_path_parts[1], bot) 245 | -------------------------------------------------------------------------------- /lart.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | lart.py - Luser Attitude Readjustment Tool 4 | Copyright 2014, Matteo Marchesotti https://www.sfwd.ws 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | https://sopel.chat 8 | """ 9 | 10 | import random 11 | 12 | from sopel import commands 13 | 14 | 15 | @commands('lart') 16 | def lart(bot, trigger): 17 | """LART (Luser Attitude Readjustment Tool). Throws a random insult to a luser! Usage: .lart """ 18 | try: 19 | collection = open('.lart.collection', 'r') 20 | except Exception as e: 21 | bot.say("No lart's collection file found. Try with .help addlart") 22 | print e 23 | return 24 | 25 | messages = [line.decode('utf-8') for line in collection.readlines()] 26 | collection.close() 27 | 28 | if len(messages)== 0: 29 | bot.say("No insult found! Type .help addlart") 30 | return; 31 | 32 | if trigger.group(2) is None: 33 | user = trigger.nick.strip() 34 | else: 35 | user = trigger.group(2).strip() 36 | 37 | message = random.choice(messages).replace('LUSER', user).encode('utf_8') 38 | 39 | bot.say(message) 40 | 41 | @commands('addlart') 42 | def addlart(bot, trigger): 43 | """Adds another insult to bot's collection with: .addlart . 'insult' _must_ contain 'LUSER' which will be substituted with the name of the luser.""" 44 | try: 45 | lart = trigger.group(2).replace('"','\"').encode('utf_8') 46 | collection = open('.lart.collection', 'a') 47 | collection.write("%s\n"%lart) 48 | collection.close() 49 | except Exception as e: 50 | bot.say("Unable to write insult lart's collection file!") 51 | print e 52 | return 53 | 54 | bot.say("Thanks %s: Insult added!"%trigger.nick.strip()) 55 | 56 | 57 | -------------------------------------------------------------------------------- /multimessage.py: -------------------------------------------------------------------------------- 1 | """ 2 | multimessage.py - Send the same message to multiple users 3 | Copyright 2013, Syfaro Warraw http://syfaro.net 4 | Licensed under the Eiffel Forum License 2. 5 | """ 6 | 7 | from sopel.module import commands, example 8 | 9 | 10 | @commands('mm', 'multimessage') 11 | @example('.mm nick1,nick2,nick3 my amazing message') 12 | def multimessage(bot, trigger): 13 | """ 14 | .mm - Sends the same message to multiple users 15 | """ 16 | if not trigger.isop: 17 | return 18 | parts = trigger.group(2).split(' ', 1) 19 | nicks = parts[0].split(',') 20 | for nick in nicks: 21 | bot.msg(nick, parts[1]) 22 | bot.reply('All messages sent!') 23 | -------------------------------------------------------------------------------- /nws.py: -------------------------------------------------------------------------------- 1 | """ 2 | warnings.py -- NWS Alert Module 3 | Copyright 2011, Michael Yanovich, yanovich.net 4 | 5 | http://sopel.chat 6 | 7 | This module allows one to query the National Weather Service for active 8 | watches, warnings, and advisories that are present. 9 | """ 10 | from __future__ import print_function 11 | from sopel.module import commands, priority 12 | import feedparser 13 | import re 14 | import urllib 15 | import sopel.web as web 16 | 17 | states = { 18 | "alabama": "al", 19 | "alaska": "ak", 20 | "arizona": "az", 21 | "arkansas": "ar", 22 | "california": "ca", 23 | "colorado": "co", 24 | "connecticut": "ct", 25 | "delaware": "de", 26 | "florida": "fl", 27 | "georgia": "ga", 28 | "hawaii": "hi", 29 | "idaho": "id", 30 | "illinois": "il", 31 | "indiana": "in", 32 | "iowa": "ia", 33 | "kansas": "ks", 34 | "kentucky": "ky", 35 | "louisiana": "la", 36 | "maine": "me", 37 | "maryland": "md", 38 | "massachusetts": "ma", 39 | "michigan": "mi", 40 | "minnesota": "mn", 41 | "mississippi": "ms", 42 | "missouri": "mo", 43 | "montana": "mt", 44 | "nebraska": "ne", 45 | "nevada": "nv", 46 | "new hampshire": "nh", 47 | "new jersey": "nj", 48 | "new mexico": "nm", 49 | "new york": "ny", 50 | "north carolina": "nc", 51 | "north dakota": "nd", 52 | "ohio": "oh", 53 | "oklahoma": "ok", 54 | "oregon": "or", 55 | "pennsylvania": "pa", 56 | "rhode island": "ri", 57 | "south carolina": "sc", 58 | "south dakota": "sd", 59 | "tennessee": "tn", 60 | "texas": "tx", 61 | "utah": "ut", 62 | "vermont": "vt", 63 | "virginia": "va", 64 | "washington": "wa", 65 | "west virginia": "wv", 66 | "wisconsin": "wi", 67 | "wyoming": "wy", 68 | } 69 | 70 | county_list = "http://alerts.weather.gov/cap/{0}.php?x=3" 71 | alerts = "http://alerts.weather.gov/cap/wwaatmget.php?x={0}" 72 | zip_code_lookup = "http://www.zip-codes.com/zip-code/{0}/zip-code-{0}.asp" 73 | nomsg = "There are no active watches, warnings or advisories, for {0}." 74 | re_fips = re.compile(r'County FIPS:(\S+)') 75 | re_state = re.compile(r'State:\S\S \[(\S+(?: \S+)?)\]') 76 | re_city = re.compile(r'City:(.*)') 77 | more_info = "Complete weather watches, warnings, and advisories for {0}, available here: {1}" 78 | 79 | 80 | @commands('nws') 81 | @priority('high') 82 | def nws_lookup(bot, trigger): 83 | """ Look up weather watches, warnings, and advisories. """ 84 | text = trigger.group(2) 85 | if not text: 86 | return 87 | bits = text.split(",") 88 | master_url = False 89 | if len(bits) == 2: 90 | ## county given 91 | url_part1 = "http://alerts.weather.gov" 92 | state = bits[1].lstrip().rstrip().lower() 93 | county = bits[0].lstrip().rstrip().lower() 94 | if state not in states: 95 | bot.reply("State not found.") 96 | return 97 | url1 = county_list.format(states[state]) 98 | page1 = web.get(url1).split("\n") 99 | for line in page1: 100 | mystr = ">" + unicode(county) + "<" 101 | if mystr in line.lower(): 102 | url_part2 = line[9:36] 103 | break 104 | if not url_part2: 105 | bot.reply("Could not find county.") 106 | return 107 | master_url = url_part1 + url_part2 108 | location = text 109 | elif len(bits) == 1: 110 | ## zip code 111 | if bits[0]: 112 | urlz = zip_code_lookup.format(bits[0]) 113 | pagez = web.get(urlz) 114 | fips = re_fips.findall(pagez) 115 | if fips: 116 | state = re_state.findall(pagez) 117 | city = re_city.findall(pagez) 118 | if not state and not city: 119 | bot.reply("Could not match ZIP code to a state") 120 | return 121 | state = state[0].lower() 122 | state = states[state].upper() 123 | location = city[0] + ", " + state 124 | fips_combo = unicode(state) + "C" + unicode(fips[0]) 125 | master_url = alerts.format(fips_combo) 126 | else: 127 | bot.reply("ZIP code does not exist.") 128 | return 129 | 130 | if not master_url: 131 | bot.reply("Invalid input. Please enter a ZIP code or a county and state pairing, such as 'Franklin, Ohio'") 132 | return 133 | 134 | feed = feedparser.parse(master_url) 135 | warnings_dict = {} 136 | for item in feed.entries: 137 | if nomsg[:51] == item["title"]: 138 | bot.reply(nomsg.format(location)) 139 | return 140 | else: 141 | warnings_dict[unicode(item["title"])] = unicode(item["summary"]) 142 | 143 | paste_code = "" 144 | for alert in warnings_dict: 145 | paste_code += item["title"] + "\n" + item["summary"] + "\n\n" 146 | 147 | paste_dict = { 148 | "paste_private": 0, 149 | "paste_code": paste_code, 150 | } 151 | 152 | pastey = urllib.urlopen("http://pastebin.com/api_public.php", 153 | urllib.urlencode(paste_dict)).read() 154 | 155 | if len(warnings_dict) > 0: 156 | if trigger.sender.startswith('#'): 157 | i = 1 158 | for key in warnings_dict: 159 | if i > 1: 160 | break 161 | bot.reply(key) 162 | bot.reply(warnings_dict[key][:510]) 163 | i += 1 164 | bot.reply(more_info.format(location, master_url)) 165 | else: 166 | for key in warnings_dict: 167 | bot.msg(trigger.nick, key) 168 | bot.msg(trigger.nick, warnings_dict[key]) 169 | bot.msg(trigger.nick, more_info.format(location, master_url)) 170 | 171 | if __name__ == '__main__': 172 | print(__doc__.strip()) 173 | -------------------------------------------------------------------------------- /oblique.py: -------------------------------------------------------------------------------- 1 | """ 2 | oblique.py - Web Services Interface 3 | Copyright 2008-9, Sean B. Palmer, inamidst.com 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.chat 7 | """ 8 | 9 | import re 10 | import urllib 11 | import sopel.web as web 12 | from sopel.module import commands, example 13 | 14 | definitions = 'https://github.com/nslater/oblique/wiki' 15 | 16 | r_item = re.compile(r'(?i)
  • (.*?)
  • ') 17 | r_tag = re.compile(r'<[^>]+>') 18 | 19 | 20 | def mappings(uri): 21 | result = {} 22 | bytes = web.get(uri) 23 | for item in r_item.findall(bytes): 24 | item = r_tag.sub('', item).strip(' \t\r\n') 25 | if not ' ' in item: 26 | continue 27 | 28 | command, template = item.split(' ', 1) 29 | if not command.isalnum(): 30 | continue 31 | if not template.startswith('http://'): 32 | continue 33 | result[command] = template.replace('&', '&') 34 | return result 35 | 36 | 37 | def service(bot, trigger, command, args): 38 | t = o.services[command] 39 | template = t.replace('${args}', urllib.quote(args.encode('utf-8'), '')) 40 | template = template.replace('${nick}', urllib.quote(trigger.nick, '')) 41 | uri = template.replace('${sender}', urllib.quote(trigger.sender, '')) 42 | 43 | info = web.head(uri) 44 | if isinstance(info, list): 45 | info = info[0] 46 | if not 'text/plain' in info.get('content-type', '').lower(): 47 | return bot.reply("Sorry, the service didn't respond in plain text.") 48 | bytes = web.get(uri) 49 | lines = bytes.splitlines() 50 | if not lines: 51 | return bot.reply("Sorry, the service didn't respond any output.") 52 | bot.say(lines[0][:350]) 53 | 54 | 55 | def refresh(bot): 56 | if hasattr(bot.config, 'services'): 57 | services = bot.config.services 58 | else: 59 | services = definitions 60 | 61 | old = o.services 62 | o.serviceURI = services 63 | o.services = mappings(o.serviceURI) 64 | return len(o.services), set(o.services) - set(old) 65 | 66 | 67 | @commands('o') 68 | @example('.o servicename arg1 arg2 arg3') 69 | def o(bot, trigger): 70 | """Call a webservice.""" 71 | if trigger.group(1) == 'urban': 72 | text = 'ud ' + trigger.group(2) 73 | else: 74 | text = trigger.group(2) 75 | 76 | if (not o.services) or (text == 'refresh'): 77 | length, added = refresh(bot) 78 | if text == 'refresh': 79 | msg = 'Okay, found %s services.' % length 80 | if added: 81 | msg += ' Added: ' + ', '.join(sorted(added)[:5]) 82 | if len(added) > 5: 83 | msg += ', &c.' 84 | return bot.reply(msg) 85 | 86 | if not text: 87 | return bot.reply('Try %s for details.' % o.serviceURI) 88 | 89 | if ' ' in text: 90 | command, args = text.split(' ', 1) 91 | else: 92 | command, args = text, '' 93 | command = command.lower() 94 | 95 | if command == 'service': 96 | msg = o.services.get(args, 'No such service!') 97 | return bot.reply(msg) 98 | 99 | if not command in o.services: 100 | return bot.reply('Service not found in %s' % o.serviceURI) 101 | 102 | if hasattr(bot.config, 'external'): 103 | default = bot.config.external.get('*') 104 | manifest = bot.config.external.get(trigger.sender, default) 105 | if manifest: 106 | commands = set(manifest) 107 | if (command not in commands) and (manifest[0] != '!'): 108 | return bot.reply('Sorry, %s is not whitelisted' % command) 109 | elif (command in commands) and (manifest[0] == '!'): 110 | return bot.reply('Sorry, %s is blacklisted' % command) 111 | service(bot, trigger, command, args) 112 | o.services = {} 113 | o.serviceURI = None 114 | 115 | @commands('snippet') 116 | def snippet(bot, trigger): 117 | if not o.services: 118 | refresh(bot) 119 | 120 | search = urllib.quote(trigger.group(2).encode('utf-8')) 121 | py = ("BeautifulSoup.BeautifulSoup(re.sub('<.*?>|(?<= ) +', '', " + 122 | "''.join(chr(ord(c)) for c in " + 123 | "eval(urllib.urlopen('http://ajax.googleapis.com/ajax/serv" + 124 | "ices/search/web?v=1.0&q=" + search + "').read()" + 125 | ".replace('null', 'None'))['responseData']['resul" + 126 | "ts'][0]['content'].decode('unicode-escape')).replace(" + 127 | "'"', '\x22')), convertEntities=True)") 128 | service(bot, trigger, 'py', py) 129 | -------------------------------------------------------------------------------- /redmine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | redmine.py - Sopel Redmine Module 4 | Copyright 2013, Ben Ramsey, benramsey.com 5 | Licensed under the MIT License. 6 | 7 | This module will respond to Redmine commands. 8 | """ 9 | 10 | from collections import OrderedDict 11 | try: 12 | from HTMLParser import HTMLParser 13 | except ImportError: # py3 14 | import html.parser as HTMLParser 15 | try: 16 | from urllib import urlencode, quote 17 | except ImportError: # py3 18 | from urllib.parse import urlencode, quote 19 | from sopel import web, tools 20 | from sopel.module import rule, commands, example 21 | import dateutil.parser 22 | import json 23 | import re 24 | 25 | pattern_url = '' 26 | 27 | def configure(config): 28 | """ 29 | To property configure the bot for Redmine access, you'll need 30 | your Redmine base URL and API access key. 31 | 32 | | [redmine] | example | purpose | 33 | | --------- | ------- | ------- | 34 | | base_url | https://example.org/redmine/ | Base URL for your Redmine installation | 35 | | api_access_key | 8843d7f92416211de9ebb963ff4ce28125932878 | Your Redmine API access key | 36 | """ 37 | if config.option('Configure Redmine?', False): 38 | if not config.has_section('redmine'): 39 | config.add_section('redmine') 40 | config.interactive_add('redmine', 'base_url', 'Redmine Base URL') 41 | config.interactive_add('redmine', 'api_access_key', 'API Access Key') 42 | 43 | 44 | def setup(bot): 45 | global pattern_url 46 | pattern_url = bot.config.redmine.base_url 47 | if not pattern_url.endswith('/'): 48 | pattern_url = pattern_url + '/' 49 | redmine = re.compile(pattern_url + '(\S+)\/(\w+)') 50 | if not bot.memory.contains('url_callbacks'): 51 | bot.memory['url_callbacks'] = tools.SopelMemory() 52 | bot.memory['url_callbacks'][redmine] = redmine_url 53 | 54 | 55 | @rule('.*https?://(\S+)/(\S+)\/(\w+).*') 56 | def redmine_url(bot, trigger): 57 | if bot.config.redmine.base_url.find(trigger.group(1)) == -1: 58 | return 59 | funcs = { 60 | 'issues': redmine_issue 61 | } 62 | try: 63 | funcs[trigger.group(2)](bot, trigger, trigger.group(3)) 64 | except: 65 | bot.say('I had trouble fetching the requested Redmine resource.') 66 | 67 | 68 | def build_url(bot, trigger, resource, resource_id, is_json=True, use_key=False, params={}): 69 | url = bot.config.redmine.base_url + resource + '/' + str(resource_id) 70 | if is_json: 71 | url += '.json' 72 | if use_key: 73 | params['key'] = bot.config.redmine.api_access_key 74 | if params: 75 | url += '?' + urlencode(params) 76 | return url 77 | 78 | 79 | @commands('rdissue') 80 | @example('.rdissue 23') 81 | def redmine_issue(bot, trigger, resource_id=None): 82 | if not resource_id: 83 | resource_id = trigger.group(2) 84 | url = build_url(bot, trigger, 'issues', resource_id, True, True, {}) 85 | try: 86 | bytes = web.get(url) 87 | result = json.loads(bytes) 88 | if 'issue' in result: 89 | issue = result['issue'] 90 | else: 91 | raise Exception('Could not find issue.') 92 | except: 93 | bot.say('I had trouble fetching the requested Redmine issue.') 94 | return 95 | 96 | try: 97 | project = issue['project']['name'] 98 | except KeyError: 99 | project = False 100 | 101 | try: 102 | tracker = issue['tracker']['name'] 103 | except KeyError: 104 | tracker = False 105 | 106 | try: 107 | assigned_to = issue['assigned_to']['name'] 108 | except KeyError: 109 | assigned_to = 'unassigned' 110 | 111 | try: 112 | author = issue['author']['name'] 113 | except KeyError: 114 | author = 'no author' 115 | 116 | try: 117 | status = issue['status']['name'] 118 | except KeyError: 119 | status = 'n/a' 120 | 121 | try: 122 | priority = issue['priority']['name'] 123 | except KeyError: 124 | priority = 'n/a' 125 | 126 | try: 127 | milestone = issue['fixed_version']['name'] 128 | except KeyError: 129 | milestone = 'no milestone' 130 | 131 | try: 132 | estimated_hours = issue['estimated_hours'] 133 | except KeyError: 134 | estimated_hours = 0.0 135 | 136 | try: 137 | spent_hours = issue['spent_hours'] 138 | except KeyError: 139 | spent_hours = 0.0 140 | 141 | try: 142 | done_ratio = issue['done_ratio'] 143 | except KeyError: 144 | done_ratio = 0 145 | 146 | try: 147 | created = dateutil.parser.parse(issue['created_on']) 148 | created = created.strftime('%Y-%m-%d') 149 | except: 150 | created = 'n/a' 151 | 152 | try: 153 | updated = dateutil.parser.parse(issue['updated_on']) 154 | updated = updated.strftime('%Y-%m-%d') 155 | except: 156 | updated = 'n/a' 157 | 158 | 159 | message = '[Redmine]' 160 | 161 | if project: 162 | message += '[' + project + ']' 163 | 164 | if tracker: 165 | message += '[' + tracker + ']' 166 | 167 | message += ' #' + str(issue['id']) + \ 168 | ' ' + issue['subject'] + \ 169 | ' | Assigned to: ' + assigned_to + \ 170 | ' | Author: ' + author + \ 171 | ' | Status: ' + status + \ 172 | ' | Priority: ' + priority + \ 173 | ' | Created: ' + created + \ 174 | ' | Updated: ' + updated + \ 175 | ' | Milestone: ' + milestone + \ 176 | ' | Done: ' + str(done_ratio) + '%' + \ 177 | ' | Estimated: ' + str(estimated_hours) + ' hrs' + \ 178 | ' | Spent: ' + str(spent_hours) + ' hrs' + \ 179 | ' <' + build_url(bot, trigger, 'issues', issue['id'], False, False, {}) + '>' 180 | 181 | bot.say(HTMLParser().unescape(message)) 182 | 183 | -------------------------------------------------------------------------------- /roulette.py: -------------------------------------------------------------------------------- 1 | """ 2 | roulette.py - Sopel Roulette Game Module 3 | Copyright 2010, Kenneth Sham 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.chat 7 | """ 8 | 9 | from __future__ import print_function 10 | from sopel.module import commands, priority 11 | import random 12 | from datetime import datetime, timedelta 13 | random.seed() 14 | 15 | # edit this setting for roulette counter. Larger, the number, the harder the game. 16 | ROULETTE_SETTINGS = { 17 | # the bigger the MAX_RANGE, the harder/longer the game will be 18 | 'MAX_RANGE': 5, 19 | 20 | # game timeout in minutes (default is 1 minute) 21 | 'INACTIVE_TIMEOUT': 1, 22 | } 23 | 24 | # edit this setting for text displays 25 | ROULETTE_STRINGS = { 26 | 'TICK': '*TICK*', 27 | 'KICK_REASON': '*SNIPED! YOU LOSE!*', 28 | 'GAME_END': 'Game stopped.', 29 | 'GAME_END_FAIL': "%s: Please wait %s seconds to stop Roulette.", 30 | } 31 | 32 | ## do not edit below this line unless you know what you're doing 33 | ROULETTE_TMP = { 34 | 'LAST-PLAYER': None, 35 | 'NUMBER': None, 36 | 'TIMEOUT': timedelta(minutes=ROULETTE_SETTINGS['INACTIVE_TIMEOUT']), 37 | 'LAST-ACTIVITY': None, 38 | } 39 | 40 | 41 | @commands('roulette') 42 | @priority('low') 43 | def roulette(bot, trigger): 44 | """Play a game of Russian Roulette""" 45 | global ROULETTE_SETTINGS, ROULETTE_STRINGS, ROULETTE_TMP 46 | if ROULETTE_TMP['NUMBER'] is None: 47 | ROULETTE_TMP['NUMBER'] = random.randint(0, ROULETTE_SETTINGS['MAX_RANGE']) 48 | ROULETTE_TMP['LAST-PLAYER'] = trigger.nick 49 | ROULETTE_TMP['LAST-ACTIVITY'] = datetime.now() 50 | bot.say(ROULETTE_STRINGS['TICK']) 51 | return 52 | if ROULETTE_TMP['LAST-PLAYER'] == trigger.nick: 53 | return 54 | ROULETTE_TMP['LAST-ACTIVITY'] = datetime.now() 55 | ROULETTE_TMP['LAST-PLAYER'] = trigger.nick 56 | if ROULETTE_TMP['NUMBER'] == random.randint(0, ROULETTE_SETTINGS['MAX_RANGE']): 57 | bot.write(['KICK', '%s %s :%s' % (trigger.sender, trigger.nick, ROULETTE_STRINGS['KICK_REASON'])]) 58 | ROULETTE_TMP['LAST-PLAYER'] = None 59 | ROULETTE_TMP['NUMBER'] = None 60 | ROULETTE_TMP['LAST-ACTIVITY'] = None 61 | else: 62 | bot.say(ROULETTE_STRINGS['TICK']) 63 | 64 | 65 | @commands('roulette-stop') 66 | @priority('low') 67 | def rouletteStop(bot, trigger): 68 | """Reset a game of Russian Roulette""" 69 | global ROULETTE_TMP, ROULETTE_STRINGS 70 | if ROULETTE_TMP['LAST-PLAYER'] is None: 71 | return 72 | if datetime.now() - ROULETTE_TMP['LAST-ACTIVITY'] > ROULETTE_TMP['TIMEOUT']: 73 | bot.say(ROULETTE_STRINGS['GAME_END']) 74 | ROULETTE_TMP['LAST-ACTIVITY'] = None 75 | ROULETTE_TMP['LAST-PLAYER'] = None 76 | ROULETTE_TMP['NUMBER'] = None 77 | else: 78 | bot.say(ROULETTE_STRINGS['GAME_END_FAIL'] % (trigger.nick, ROULETTE_TMP['TIMEOUT'].seconds - (datetime.now() - ROULETTE_TMP['LAST-ACTIVITY']).seconds)) 79 | 80 | 81 | if __name__ == '__main__': 82 | print(__doc__.strip()) 83 | -------------------------------------------------------------------------------- /slap.py: -------------------------------------------------------------------------------- 1 | """ 2 | slap.py - Slap Module 3 | Copyright 2009, Michael Yanovich, yanovich.net 4 | 5 | http://sopel.chat 6 | """ 7 | 8 | import random 9 | import re 10 | from sopel.module import commands 11 | from sopel.tools import Identifier 12 | 13 | 14 | @commands('slap', 'slaps') 15 | def slap(bot, trigger): 16 | """.slap - Slaps """ 17 | text = trigger.group().split() 18 | if len(text) < 2: 19 | text.append(trigger.nick) 20 | text[1] = re.sub(r"\x1f|\x02|\x12|\x0f|\x16|\x03(?:\d{1,2}(?:,\d{1,2})?)?", '', text[1]) 21 | if text[1].startswith('#'): 22 | return 23 | if text[1] == 'me' or text[1] == 'myself': 24 | text[1] = trigger.nick 25 | try: 26 | if Identifier(text[1]) not in bot.privileges[trigger.sender.lower()]: 27 | bot.say("You can't slap someone who isn't here!") 28 | return 29 | except KeyError: 30 | pass 31 | if text[1] == bot.nick: 32 | if (not trigger.admin): 33 | text[1] = trigger.nick 34 | else: 35 | text[1] = 'itself' 36 | if text[1] in bot.config.core.admins: 37 | if (not trigger.admin): 38 | text[1] = trigger.nick 39 | verb = random.choice(('slaps', 'kicks', 'destroys', 'annihilates', 'punches', 'roundhouse kicks', 'pwns', 'owns')) 40 | bot.write(['PRIVMSG', trigger.sender, ' :\x01ACTION', verb, text[1], '\x01']) 41 | -------------------------------------------------------------------------------- /twit.py: -------------------------------------------------------------------------------- 1 | """ 2 | twitter.py - Sopel Twitter Module 3 | Copyright 2008-10, Michael Yanovich, opensource.osu.edu/~yanovich/wiki/ 4 | Copyright 2011, Edward Powell, embolalia.net 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://sopel.chat 8 | """ 9 | from __future__ import print_function 10 | import tweepy 11 | import time 12 | import re 13 | from sopel.config import ConfigurationError 14 | from sopel import tools 15 | from sopel.module import rule 16 | import sys 17 | if sys.version_info.major < 3: 18 | str = unicode 19 | 20 | try: 21 | import html 22 | except ImportError: 23 | import HTMLParser 24 | html = HTMLParser.HTMLParser() 25 | unescape = html.unescape 26 | 27 | 28 | def configure(config): 29 | """ 30 | These values are all found by signing up your bot at 31 | [https://dev.twitter.com/apps/new](https://dev.twitter.com/apps/new). 32 | 33 | | [twitter] | example | purpose | 34 | | --------- | ------- | ------- | 35 | | consumer_key | 09d8c7b0987cAQc7fge09 | OAuth consumer key | 36 | | consumer_secret | LIaso6873jI8Yasdlfi76awer76yhasdfi75h6TFJgf | OAuth consumer secret | 37 | | access_token | 564018348-Alldf7s6598d76tgsadfo9asdf56uUf65aVgdsf6 | OAuth access token | 38 | | access_token_secret | asdfl7698596KIKJVGvJDcfcvcsfdy85hfddlku67 | OAuth access token secret | 39 | """ 40 | 41 | if config.option('Configure Twitter? (You will need to register on http://api.twitter.com)', False): 42 | config.interactive_add('twitter', 'consumer_key', 'Consumer key') 43 | config.interactive_add('twitter', 'consumer_secret', 'Consumer secret') 44 | config.interactive_add('twitter', 'access_token', 'Access token') 45 | config.interactive_add('twitter', 'access_token_secret', 'Access token secret') 46 | 47 | 48 | def setup(sopel): 49 | try: 50 | auth = tweepy.OAuthHandler(sopel.config.twitter.consumer_key, willie.config.twitter.consumer_secret) 51 | auth.set_access_token(sopel.config.twitter.access_token, willie.config.twitter.access_token_secret) 52 | api = tweepy.API(auth) 53 | except: 54 | raise ConfigurationError('Could not authenticate with Twitter. Are the' 55 | ' API keys configured properly?') 56 | regex = re.compile('twitter.com\/(\S*)\/status\/([\d]+)') 57 | if not sopel.memory.contains('url_callbacks'): 58 | sopel.memory['url_callbacks'] = tools.SopelMemory() 59 | sopel.memory['url_callbacks'][regex] = gettweet 60 | 61 | 62 | def format_thousands(integer): 63 | """Returns string of integer, with thousands separated by ','""" 64 | return re.sub(r'(\d{3})(?=\d)', r'\1,', str(integer)[::-1])[::-1] 65 | 66 | def tweet_url(status): 67 | """Returns a URL to Twitter for the given status object""" 68 | return 'https://twitter.com/' + status.user.screen_name + '/status/' + status.id_str 69 | 70 | @rule('.*twitter.com\/(\S*)\/status\/([\d]+).*') 71 | def gettweet(sopel, trigger, found_match=None): 72 | """Show the last tweet by the given user""" 73 | try: 74 | auth = tweepy.OAuthHandler(sopel.config.twitter.consumer_key, willie.config.twitter.consumer_secret) 75 | auth.set_access_token(sopel.config.twitter.access_token, willie.config.twitter.access_token_secret) 76 | api = tweepy.API(auth) 77 | 78 | if found_match: 79 | status = api.get_status(found_match.group(2), tweet_mode='extended') 80 | else: 81 | parts = trigger.group(2).split() 82 | if parts[0].isdigit(): 83 | status = api.get_status(parts[0], tweet_mode='extended') 84 | else: 85 | twituser = parts[0] 86 | twituser = str(twituser) 87 | statusnum = 0 88 | if len(parts) > 1 and parts[1].isdigit(): 89 | statusnum = int(parts[1]) - 1 90 | status = api.user_timeline(twituser, tweet_mode='extended')[statusnum] 91 | twituser = '@' + status.user.screen_name 92 | 93 | # 280-char BS 94 | try: 95 | text = status.full_text 96 | except: 97 | try: 98 | text = status.text 99 | except: 100 | return sopel.reply("I couldn't find the tweet text. :/") 101 | 102 | try: 103 | for media in status.entities['media']: 104 | text = text.replace(media['url'], media['media_url']) 105 | except KeyError: 106 | pass 107 | try: 108 | for url in status.entities['urls']: 109 | text = text.replace(url['url'], url['expanded_url']) 110 | except KeyError: 111 | pass 112 | sopel.say(twituser + ": " + str(text) + ' <' + tweet_url(status) + '>') 113 | except: 114 | sopel.reply("You have inputted an invalid user.") 115 | gettweet.commands = ['twit'] 116 | gettweet.priority = 'medium' 117 | gettweet.example = '.twit aplusk [tweetNum] or .twit 381982018927853568' 118 | 119 | def f_info(sopel, trigger): 120 | """Show information about the given Twitter account""" 121 | try: 122 | auth = tweepy.OAuthHandler(sopel.config.twitter.consumer_key, willie.config.twitter.consumer_secret) 123 | auth.set_access_token(sopel.config.twitter.access_token, willie.config.twitter.access_token_secret) 124 | api = tweepy.API(auth) 125 | 126 | twituser = trigger.group(2) 127 | twituser = str(twituser) 128 | if '@' in twituser: 129 | twituser = twituser.translate(None, '@') 130 | 131 | info = api.get_user(twituser) 132 | friendcount = format_thousands(info.friends_count) 133 | name = info.name 134 | id = info.id 135 | favourites = info.favourites_count 136 | followers = format_thousands(info.followers_count) 137 | location = info.location 138 | description = unescape(info.description) 139 | sopel.reply("@" + str(twituser) + ": " + str(name) + ". " + "ID: " + str(id) + ". Friend Count: " + friendcount + ". Followers: " + followers + ". Favourites: " + str(favourites) + ". Location: " + str(location) + ". Description: " + str(description)) 140 | except: 141 | sopel.reply("You have inputted an invalid user.") 142 | f_info.commands = ['twitinfo'] 143 | f_info.priority = 'medium' 144 | f_info.example = '.twitinfo aplsuk' 145 | 146 | def f_update(sopel, trigger): 147 | """Tweet with Sopel's account. Admin-only.""" 148 | if trigger.admin: 149 | auth = tweepy.OAuthHandler(sopel.config.twitter.consumer_key, willie.config.twitter.consumer_secret) 150 | auth.set_access_token(sopel.config.twitter.access_token, willie.config.twitter.access_token_secret) 151 | api = tweepy.API(auth) 152 | 153 | print(api.me().name) 154 | 155 | update = str(trigger.group(2)) + " ^" + trigger.nick 156 | if len(update) <= 140: 157 | api.update_status(update) 158 | sopel.reply("Successfully posted to my twitter account.") 159 | else: 160 | toofar = len(update) - 140 161 | sopel.reply("Please shorten the length of your message by: " + str(toofar) + " characters.") 162 | f_update.commands = ['tweet'] 163 | f_update.priority = 'medium' 164 | f_update.example = '.tweet Hello World!' 165 | 166 | def f_reply(sopel, trigger): 167 | auth = tweepy.OAuthHandler(sopel.config.twitter.consumer_key, willie.config.twitter.consumer_secret) 168 | auth.set_access_token(sopel.config.twitter.access_token, willie.config.twitter.access_token_secret) 169 | api = tweepy.API(auth) 170 | 171 | incoming = str(trigger.group(2)) 172 | incoming = incoming.split() 173 | statusid = incoming[0] 174 | if statusid.isdigit(): 175 | update = incoming[1:] 176 | if len(update) <= 140: 177 | statusid = int(statusid) 178 | #api3.PostUpdate(str(" ".join(update)), in_reply_to_status_id=10503164300) 179 | sopel.reply("Successfully posted to my twitter account.") 180 | else: 181 | toofar = len(update) - 140 182 | sopel.reply("Please shorten the length of your message by: " + str(toofar) + " characters.") 183 | else: 184 | sopel.reply("Please provide a status ID.") 185 | #f_reply.commands = ['reply'] 186 | f_reply.priority = 'medium' 187 | f_reply.example = '.reply 892379487 I like that idea!' 188 | 189 | if __name__ == '__main__': 190 | print(__doc__.strip()) 191 | -------------------------------------------------------------------------------- /whois.py: -------------------------------------------------------------------------------- 1 | """ 2 | whois.py - Sopel Whois module 3 | Copyright 2014, Ellis Percival (Flyte) sopel@failcode.co.uk 4 | Licensed under the Eiffel Forum License 2. 5 | 6 | http://sopel.chat 7 | 8 | A module to enable Sopel to perform WHOIS lookups on nicknames. 9 | This can either be to have Sopel perform lookups on behalf of 10 | other people, or can be imported and used by other modules. 11 | """ 12 | 13 | from sopel.module import commands, event, rule 14 | from time import sleep 15 | from datetime import datetime, timedelta 16 | 17 | AGE_THRESHOLD = timedelta(days=1) 18 | 19 | class Whois(object): 20 | def __init__(self, data): 21 | to, self.nick, self.ident, self.host, star, self.name = data 22 | self.datetime = datetime.now() 23 | 24 | def __repr__(self): 25 | return "%s(nick=%r, ident=%r, host=%r, name=%r, datetime=%r)" % ( 26 | self.__class__.__name__, 27 | self.nick, 28 | self.ident, 29 | self.host, 30 | self.name, 31 | self.datetime 32 | ) 33 | 34 | def __str__(self): 35 | return "%s!%s@%s * %s" % ( 36 | self.nick, self.ident, self.host, self.name) 37 | 38 | 39 | class WhoisFailed(Exception): 40 | pass 41 | 42 | 43 | def setup(bot): 44 | bot.memory["whois"] = {} 45 | 46 | def _clear_old_entries(bot): 47 | """ 48 | Removes entries from the bot's memory which are older 49 | than AGE_THRESHOLD. 50 | """ 51 | to_del = [] 52 | for nick, whois in bot.memory["whois"].items(): 53 | if whois.datetime < datetime.now() - AGE_THRESHOLD: 54 | to_del.append(nick) 55 | for nick in to_del: 56 | try: 57 | del bot.memory["whois"][nick] 58 | except KeyError: 59 | pass 60 | 61 | def send_whois(bot, nick): 62 | """ 63 | Sends the WHOIS command to the server for the 64 | specified nick. 65 | """ 66 | bot.write(["WHOIS", nick]) 67 | 68 | def get_whois(bot, nick): 69 | """ 70 | Waits for the response to be put into the bot's 71 | memory by the receiving thread. 72 | """ 73 | i = 0 74 | while nick not in bot.memory["whois"] and i < 10: 75 | i += 1 76 | sleep(1) 77 | 78 | if nick not in bot.memory["whois"]: 79 | raise WhoisFailed("No reply from server") 80 | elif bot.memory["whois"][nick] is None: 81 | try: 82 | del bot.memory["whois"][nick] 83 | except KeyError: 84 | pass 85 | raise WhoisFailed("No such nickname") 86 | 87 | # A little housekeeping 88 | _clear_old_entries(bot) 89 | 90 | return bot.memory["whois"][nick] 91 | 92 | def whois(bot, nick): 93 | """ 94 | Sends the WHOIS command to the server then waits for 95 | the response to be put into the bot's memory by the 96 | receiving thread. 97 | """ 98 | # Remove entry first so that we get the latest 99 | try: 100 | del bot.memory["whois"][nick] 101 | except KeyError: 102 | pass 103 | send_whois(bot, nick) 104 | return get_whois(bot, nick) 105 | 106 | @rule(r".*") 107 | @event("311") 108 | def whois_found_reply(bot, trigger): 109 | """ 110 | Listens for successful WHOIS responses and saves 111 | them to the bot's memory. 112 | """ 113 | nick = trigger.args[1] 114 | bot.memory["whois"][nick] = Whois(trigger.args) 115 | 116 | @rule(r".*") 117 | @event("401") 118 | def whois_not_found_reply(bot, trigger): 119 | """ 120 | Listens for unsuccessful WHOIS responses and saves 121 | None to the bot's memory so that the initial 122 | whois function is aware that the lookup failed. 123 | """ 124 | nick = trigger.args[1] 125 | bot.memory["whois"][nick] = None 126 | 127 | # Give the initiating whois function time to see 128 | # that the lookup has failed, then remove the None. 129 | sleep(5) 130 | try: 131 | del bot.memory["whois"][nick] 132 | except KeyError: 133 | pass --------------------------------------------------------------------------------