ibid-0.1.1/0000755000000000000000000000000011531277655011132 5ustar rootrootibid-0.1.1/factpacks/0000755000000000000000000000000011531311312013044 5ustar rootrootibid-0.1.1/factpacks/knab.py0000755000000000000000000000060411362435477014361 0ustar rootroot#!/usr/bin/env python # Copyright (c) 2009, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. # # Usage: knab.py [...] from sys import argv import simplejson as json values = [' ' + line.strip() for line in open(argv[1]).readlines()] names = argv[2:] print json.dumps([[names, values]], indent=1) ibid-0.1.1/factpacks/knab-verbs.py0000755000000000000000000000175611362435477015511 0ustar rootroot#!/usr/bin/env python # Copyright (c) 2009, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import re from collections import defaultdict from sys import argv try: import json except ImportError: import simplejson as json def default(): return ([], []) factoids = defaultdict(default) for line in open(argv[1]): line = line.strip() match = re.match(r'^verb (.+) (\d+)$', line) if match: name, id = match.groups() factoids[int(id)][0].append(name) continue match = re.match(r'^(\d+) (action|reply) (.+)$', line) if match: id, action, value = match.groups() factoids[int(id)][1].append('<%s> %s' % (action, value.replace('##', '$1'))) for names, values in factoids.values(): for value in values: if '$1' in value: for index, name in enumerate(names): names[index] = name + ' $arg' break print json.dumps(factoids.values(), indent=1) ibid-0.1.1/factpacks/roshambo.json0000644000000000000000000000072011362435477015575 0ustar rootroot[ [["roshambo rock"], [" $who: You win! I chose scissors :-(", " $who: We drew! I also chose rock", " $who: I win! I chose paper"]], [["roshambo paper"], [" $who: You win! I chose rock :-(", " $who: We drew! I also chose paper", " $who: I win! I chose scissors"]], [["roshambo scissors"], [" $who: You win! I chose paper :-(", " $who: We drew! I also chose scissors", " $who: I win! I chose rock"]] ] ibid-0.1.1/factpacks/greetings.json0000644000000000000000000000031711362435477015754 0ustar rootroot[ [["How are you"], [" $who: I'm doing well thanks", " $who: Same as yesterday"]], [["Bye", "kbye", "Night", "Good Night"], [" kbye $who", " $who: Bye", " Cheers $who"]] ] ibid-0.1.1/factpacks/smtp.json0000644000000000000000000000376511362435477014762 0ustar rootroot[ [["smtp 1xx"],["indicates the mail server has accepted the command, but does not yet take any action. A confirmation message is required."]], [["smtp 2xx"],["Mail server has completed the task successfully without errors"]], [["smtp 211"],["System status, or system help reply"]], [["smtp 214"],["Help message"]], [["smtp 220"],[" Service ready"]], [["smtp 221"],[" Service closing transmission channel"]], [["smtp 250"],["Requested mail action okay, completed"]], [["smtp 251"],["User not local; will forward to "]], [["smtp 252"],["Cannot VRFY user, but will accept message and attempt"]], [["smtp 3xx"],["indicates the mail server has understood the request, but requires further information to complete it"]], [["smtp 354"],["Start mail input; end with ."]], [["smtp 4xx"],["indicates the mail server has encountered a temporary failure. If the command is repeated without any change, it might be completed. Try again, it may help!"]], [["smtp 421"],[" Service not available, closing transmission channel"]], [["smtp 450"],["Requested mail action not taken: mailbox unavailable"]], [["smtp 451"],["Requested action aborted: local error in processing"]], [["smtp 452"],["Requested action not taken: insufficient system storage"]], [["smtp 454"],["TLS not available due to temporary reason"]], [["smtp 5xx"],["indicates the mail server has encountered a fatal error. Your request can't be processed"]], [["smtp 500"],["Syntax error, command unrecognized"]], [["smtp 501"],["Syntax error in parameters or arguments"]], [["smtp 502"],["Command not implemented"]], [["smtp 503"],["Bad sequence of commands"]], [["smtp 504"],["Command parameter not implemented"]], [["smtp 550"],["Requested action not taken: mailbox unavailable"]], [["smtp 551"],["User not local; please try "]], [["smtp 552"],["Requested mail action aborted: exceeded storage allocation"]], [["smtp 553"],["Requested action not taken: mailbox name not allowed"]], [["smtp 554"],["Transaction failed"]] ] ibid-0.1.1/factpacks/divine.json0000644000000000000000000000213111362435477015237 0ustar rootroot[ [["divine $arg"], [ " $who: Outlook Not So Good", " $who: My Reply Is No", " $who: Don't Count On It", " $who: You May Rely On It", " $who: Most Likely", " $who: Yes", " $who: Yes Definitely", " $who: It Is Certain", " $who: Very Doubtful", " $who: It Is Decidedly So", " $who: Signs Point to Yes", " $who: My Sources Say No", " $who: Without a Doubt", " $who: As I See It, Yes", " $who: Obviously", " $who: Yeah, and I'm the Pope", " $who: That's ridiculous", " $who: Forget about it", " $who: You wish", " $who: Yeah, right", " $who: In your dreams", " $who: You've got to be kidding", " $who: Dumb question. Ask another", " $who: Not a chance", " $who: Outlook Sucks", " $who: Bugger Off, No Ways", " $who: When hell freezes over", " $who: 100% For Sure", " $who: I Think So", " $who: Absolutely", " $who: All the way", " $who: Duh", " $who: Yep", " $who: Uh Huh" ]] ] ibid-0.1.1/factpacks/README0000644000000000000000000000460511362435477013756 0ustar rootrootIbid Factpacks are a way to distribute packages of related factoids. They can be installed and removed collectively using the ibid-factpack utility. Format ------ JSON-formatted files containing a list of factoids. Each factoid is a (names, values) pair. names is a list of strings: names for the factoid. values is a list of strings: contents of the factoid. Copyright --------- divine.json: Copyright (c) 2008-2010, Jonathan Hitchcock, Stefano Rivera Released under terms of the same terms as Perl itself (the Artistic License). This software is meant to be freely available under those terms in perpetuity. Derived from knab: Copyright (c) 2003-2009 Jonathan Hitchcock Derived from infobot: Copyright (c) 1996-2000, Sarah Burcham, Kevin Lenzo greetings.json: Copyright (c) 2008-2010, Jonathan Hitchcock, Michael Gorven, Stefano Rivera Released under terms of the MIT/X/Expat Licence. See COPYING for details. handey.json: Copyright (c), Jack Handey http.json: Copyright (c) 2008-2010, Jonathan Hitchcock Released under terms of the MIT/X/Expat Licence. See COPYING for details. overlord.json: This Evil Overlord List is Copyright 1996-1997 by Peter Anspach. If you enjoy it, feel free to pass it along or post it anywhere, provided that (1) it is not altered in any way, and (2) this copyright notice is attached. roshambo.json: Copyright (c) 2008-2010, Adrian Moisey, Michael Gorven Released under terms of the MIT/X/Expat Licence. See COPYING for details. rubaiyat.json: Copyright (c) 2003-2010, Jonathan Hitchcock Released under terms of the MIT/X/Expat Licence. See COPYING for details. Derived from a work in the Public Domain. smtp.json: Copyright (c) 2008-2010, Jonathan Hitchcock, Michael Gorven Released under terms of the MIT/X/Expat Licence. See COPYING for details. tao.json: Copyright (c) 2003-2010, Jonathan Hitchcock Released under terms of the MIT/X/Expat Licence. See COPYING for details. Derived from a work in the Public Domain. verbs.json: Copyright (c) 2003-2010, Jonathan Hitchcock Released under terms of the MIT/X/Expat Licence. See COPYING for details. wrestling.json: Copyright Unknown, widely distributed on the Internet. zen.json: Copyright (c) 2008-2010, Michael Gorven Released under terms of the MIT/X/Expat Licence. See COPYING for details. Derived from a work in the Public Domain. zippy.json: Copyright Bill Griffith, widely distributed on the Internet. ibid-0.1.1/factpacks/rubaiyat.json0000644000000000000000000003325011362435477015607 0ustar rootroot[ [ [ "rubaiyat" ], [ " Awake! for Morning in the Bowl of Night/Has flung the Stone that puts the Stars to Flight:/And Lo! the Hunter of the East has caught/The Sultan's Turret in a Noose of Light.", " Dreaming when Dawn's Left Hand was in the Sky/I heard a Voice within the Tavern cry,/\"Awake, my Little ones, and fill the Cup/\"Before Life's Liquor in its Cup be dry.\"", " And, as the Cock crew, those who stood before/The Tavern shouted - \"Open then the Door!/\"You know how little while we have to stay,/\"And, once departed, may return no more.\"", " Now the New Year reviving old Desires,/The thoughtful Soul to Solitude retires,/Where the WHITE HAND OF MOSES on the Bough/Puts out, and Jesus from the Ground suspires.", " Iram indeed is gone with all its Rose,/And Jamshyd's Sev'n-ring'd Cup where no one knows;/But still the Vine her ancient Ruby yields,/And still a Garden by the Water blows.", " And David's Lips are lock't; but in divine/High piping Pehlevi, with \"Wine! Wine! Wine!/\"Red Wine!\" - the Nightingale cries to the Rose/That yellow Cheek of hers to incarnadine.", " Come, fill the Cup, and in the Fire of Spring/The Winter Garment of Repentance fling:/The Bird of Time has but a little way/To fly - and Lo! the Bird is on the Wing.", " And look - a thousand Blossoms with the Day/Woke - and a thousand scatter'd into Clay:/And this first Summer Month that brings the Rose/Shall take Jamshyd and Kaikobad away.", " But come with old Khayyam, and leave the Lot/Of Kaikobad and Kaikhosru forgot!/Let Rustum lay about him as he will,/Or Hatim Tai cry Supper - heed them not.", " With me along some Strip of Herbage strown/That just divides the desert from the sown,/Where name of Slave and Sultan scarce is known,/And pity Sultan Mahmud on his Throne.", " Here with a Loaf of Bread beneath the Bough,/A Flask of Wine, a Book of Verse - and Thou/Beside me singing in the Wilderness - /And Wilderness is Paradise enow.", " \"How sweet is mortal Sovranty!\" - think some:/Others - \"How blest the Paradise to come!\"/Ah, take the Cash in hand and waive the Rest;/Oh, the brave Music of a distant Drum!", " Look to the Rose that blows about us - \"Lo,/\"Laughing,\" she says, \"into the World I blow:/\"At once the silken Tassel of my Purse/\"Tear, and its Treasure on the Garden throw.\"", " The Worldly Hope men set their Hearts upon/Turns Ashes - or it prospers; and anon,/Like Snow upon the Desert's dusty Face/Lighting a little Hour or two - is gone.", " And those who husbanded the Golden Grain,/And those who flung it to the Winds like Rain,/Alike to no such aureate Earth are turn'd/As, buried once, Men want dug up again.", " Think, in this batter'd Caravanserai/Whose Doorways are alternate Night and Day,/How Sultan after Sultan with his Pomp/Abode his Hour or two, and went his way.", " They say the Lion and the Lizard keep/The Courts where Jamshyd gloried and drank deep;/And Bahram, that great Hunter - the Wild Ass/Stamps o'er his Head, and he lies fast asleep.", " I sometimes think that never so red/The Rose as where some buried Caesar bled;/That every Hyacinth the Garden wears/Dropt in its Lap from some once lovely Head.", " And this delightful Herb whose tender Green/Fledges the River's Lip on which we lean - /Ah, lean upon it lightly! for who knows/From what once lovely Lip it springs unseen!", " Ah, my Beloved, fill the Cup that clears/TO-DAY of past Regrets and future Fears - /To-morrow? - Why, To-morrow I may be/Myself with Yesterday's Sev'n Thousand Years.", " Lo! some we loved, the loveliest and best/That Time and Fate of all their Vintage prest,/Have drunk their Cup a Round or two before,/And one by one crept silently to Rest.", " Ah, make the most of what we yet may spend,/Before we too into the Dust descend;/Dust into Dust, and under Dust, to lie,/Sans Wine, sans Song, sans Singer, and - sans End!", " Alike for those who for TO-DAY prepare,/And those that after a TO-MORROW stare,/A Muezzin from the Tower of Darkness cries/\"Fools! your Reward is neither Here nor There!\"", " Why, all the Saints and Sages who discuss'd/Of the Two Worlds so learnedly, are thrust/Like foolish Prophets forth; their Words to Scorn/Are scatter'd, and their Mouths are stopt with Dust.", " And we, that now make merry in the Room/They left, and Summer dresses in new Bloom,/Ourselves must we beneath the Couch of Earth/Descend, ourselves to make a Couch - for whom?", " Oh, come with old Khayyam, and leave the Wise/To talk; one thing is certain, that Life flies;/One thing is certain, and the Rest is Lies;/The Flower that once has blown for ever dies.", " Myself when young did eagerly frequent/Doctor and Saint, and heard great Argument/About it and about: but evermore/Came out by the same Door as in I went.", " With them the Seed of Wisdom did I sow,/And with my own hand labour'd it to grow:/And this was all the Harvest that I reap'd - /\"I came like Water, and like Wind I go.\"", " Into this Universe, and why not knowing,/Nor whence, like Water willy-nilly flowing:/And out of it, as Wind along the Waste,/I know not whither, willy-nilly blowing.", " What, without asking, hither hurried whence?/And, without asking, whither hurried hence!/Another and another Cup to drown/The Memory of this Impertinence!", " Up from Earth's Centre through the Seventh Gate/I rose, and on the Throne of Saturn sate,/And many Knots unravel'd by the Road;/But not the Knot of Human Death and Fate.", " There was a Door to which I found no Key:/There was a Veil past which I could not see:/Some little Talk awhile of ME and THEE/There seemed - and then no more of THEE and ME.", " Then to the rolling Heav'n itself I cried,/Asking, \"What Lamp had Destiny to guide/\"Her little Children stumbling in the Dark?\"/And - \"A blind Understanding!\" Heav'n replied.", " Then to this earthen Bowl did I adjourn/My Lip the secret Well of Life to learn:/And Lip to Lip it murmur'd - \"While you live/\"Drink! - for once dead you never shall return.\"", " I think the Vessel, that with fugitive/Articulation answer'd, once did live,/And merry-make; and the cold Lip I kiss'd/How many Kisses might it take - and give!", " For in the Market-place, one Dusk of Day,/I watch'd the Potter thumping his wet Clay:/And with its all obliterated Tongue/It murmur'd - \"Gently, Brother, gently, pray!\"", " Ah, fill the Cup: - what boots it to repeat/How Time is slipping underneath our Feet:/Unborn TO-MORROW, and dead YESTERDAY,/Why fret about them if TO-DAY be sweet!", " One Moment in Annihilation's Waste,/One Moment, of the Well of Life to taste - /The Stars are setting and the Caravan/Starts for the Dawn of Nothing - Oh, make haste!", " How long, how long, in infinite Pursuit/Of This and That endeavour and dispute?/Better be merry with the fruitful Grape/Than sadden after none, or bitter, Fruit.", " You know, my Friends, how long since in my House/For a new Marriage I did make Carouse:/Divorced old barren Reason from my Bed,/And took the Daughter of the Vine to Spouse.", " For \"IS\" and \"IS-NOT\" though with Rule and Line,/And \"UP-AND-DOWN\" without, I could define,/I yet in all I only cared to know,/Was never deep in anything but - Wine.", " And lately, by the Tavern Door agape,/Came stealing through the Dusk an Angel Shape/Bearing a Vessel on his Shoulder; and/He bid me taste of it; and 'twas - the Grape!", " The Grape that can with Logic absolute/The Two-and-Seventy jarring Sects confute:/The subtle Alchemist that in a Trice/Life's leaden Metal into Gold transmute.", " The mighty Mahmud, the victorious Lord,/That all the misbelieving and black Horde/Of Fears and Sorrows that infest the Soul/Scatters and slays with his enchanted Sword.", " But leave the Wise to wrangle, and with me/The Quarrel of the Universe let be:/And, in some corner of the Hubbub coucht,/Make Game of that which makes as much of Thee.", " For in and out, above, about, below,/'Tis nothing but a Magic Shadow-show,/Play'd in a Box whose Candle is the Sun,/Round which we Phantom Figures come and go.", " And if the Wine you drink, the Lip you press,/End in the Nothing all Things end in - Yes - /Then fancy while Thou art, Thou art but what/Thou shalt be - Nothing - Thou shalt not be less.", " While the Rose blows along the River Brink,/With old Khayyam the Ruby Vintage drink:/And when the Angel with his darker Draught/Draws up to Thee - take that, and do not shrink.", " 'Tis all a Chequer-board of Nights and Days/Where Destiny with Men for Pieces plays:/Hither and thither moves, and mates, and slays,/And one by one back in the Closet lays.", " The Ball no Question makes of Ayes and Noes,/But Right or Left, as strikes the Player goes;/And He that toss'd Thee down into the Field,/*He* knows about it all - He knows - HE knows!", " The Moving Finger writes; and, having writ,/Moves on: nor all thy Piety nor Wit/Shall lure it back to cancel half a Line,/Nor all thy Tears wash out a Word of it.", " And that inverted Bowl we call The Sky,/Whereunder crawling coop't we live and die,/Lift not thy hands to *It* for help - for It/Rolls impotently on as Thou or I.", " With Earth's first Clay They did the Last Man's knead,/And then of the Last Harvest sow'd the Seed:/Yea, the first Morning of Creation wrote/What the Last Dawn of Reckoning shall read.", " I tell Thee this - When, starting from the Goal,/Over the shoulders of the flaming Foal/Of Heav'n Parvin and Mushtara they flung,/In my predestin'd Plot of Dust and Soul", " The Vine had struck a Fibre; which about/If clings my Being - let the Sufi flout;/Of my Base Metal may be filed a Key,/That shall unlock the Door he howls without", " And this I know: whether the one True Light,/Kindle to Love, or Wrathconsume me quite,/One Glimpse of It within the Tavern caught/Better than in the Temple lost outright.", " Oh, Thou, who Man of baser Earth didst make,/And who with Eden didst devise the Snake;/For all the Sin wherewith the Face of Man/Is blacken'd, Man's Forgiveness give - and take!", " Listen again. One Evening at the Close/Of Ramazan, ere the better Moon arose,/In that old Potter's Shop I stood alone/With the clay Population round in Rows.", " Oh, Thou, who didst with Pitfall and with Gin/Beset the Road I was to wander in,/Thou wilt not with Predestination round/Enmesh me, and impute my Fall to Sin?", " And, strange to tell, among that Earthen Lot/Some could articulate, while others not:/And suddenly one more impatient cried - /\"Who *is* the Potter, pray, and who the Pot?\"", " Then said another - \"Surely not in vain/\"My Substance from the common Earth was ta'en,/\"That He who subtly wrought me into Shape/\"Should stamp me back to common Earth again.\"", " Another said - \"Why, ne'er a peevish Boy,/\"Would break the Bowl from which he drank in Joy;/\"Shall He that *made* the Vessel in pure Love/\"And Fancy, in an after Rage destroy!\"", " None answer'd this; but after Silence spake/A Vessel of a more ungainly Make:/\"They sneer at me for learning all awry;/\"What! did the Hand then of the Potter shake?\"", " Said one - \"Folk of a surly Tapster tell/\"And daub his Visage with the Smoke of Hell;/\"They talk of some strict Testing of us - Pish!/\"He's a Good Fellow, and 't will all be well.\"", " Then said another with a long-drawn Sigh,/\"My Clay with long oblivion is gone dry:/\"But, fill me with the old familiar Juice,/\"Methinks I might recover by-and-bye!\"", " So while the Vessels one by one were speaking,/One spied the little Crescent all were seeking:/And then they jogg'd each other, \"Brother! Brother!/\"Hark to the Porter's Shoulder-knot a-creaking!\"", " Ah, with the Grape my fading Life provide,/And wash my Body whence the Life has died,/And in the Windingsheet of Vine-leaf wrapt,/So bury me by some sweet Garden-side.", " That ev'n my buried Ashes such a Snare/Of Perfume shall fling up into the Air,/As not a True Believer passing by/But shall be overtaken unaware.", " Indeed the Idols I have loved so long/Have done my Credit in Men's Eye much wrong:/Have drown'd my Honour in a shallow Cup,/And sold my Reputation for a Song.", " Indeed, indeed, Repentance oft before/I swore - but was I sober when I swore?/And then and then came Spring, and Rose-in-hand/My thread-bare Penitence apieces tore.", " And much as Wine has play'd the Infidel/And robb'd me of my Robe of Honour - well,/I often wonder what the Vintners buy/One half so precious as the Goods they sell.", " Alas, that Spring should vanish with the Rose!/That Youth's sweet-scented Manuscript should close!/The Nightingale that in the Branches sang,/Ah, whence, and whither flown again, who knows!", " Ah Love! could thou and I with Fate conspire/To grasp this sorry Scheme of Things entire,/Would not we shatter it to bits - and then/Re-mould it nearer to the Heart's Desire!", " Ah, Moon of my Delight who Know'st no wane/The Moon of Heav'n is rising once again:/How oft hereafter rising shall she look/Through this same Garden after me - in vain!", " And when Thyself with shining Foot shall pass/Among the Guests Star-scatter'd on the Grass,/And in thy joyous Errand reach the Spot/Where I made one - turn down an empty Glass!" ] ] ] ibid-0.1.1/factpacks/tao.json0000644000000000000000000010774111362435477014561 0ustar rootroot[ [ [ "tao" ], [ " Can you coax your mind from its wandering/and keep to the original oneness?/Can you let your body become/supple as a newborn child's?/Can you cleanse your inner vision/until you see nothing but the light?/Can you love people and lead them/without imposing your will?/Can you deal with the most vital matters/by letting events take their course?/Can you step back from your own mind/and thus understand all things?/Giving birth and nourishing,/having without possessing,/acting with no expectations,/leading and not trying to control:/this is the supreme virtue.", " The ancient Masters were profound and subtle./Their wisdom was unfathomable./There is no way to describe it;/all we can describe is their appearance./They were careful/as someone crossing an iced-over stream./Alert as a warrior in enemy territory./Courteous as a guest./Fluid as melting ice./Shapable as a block of wood./Receptive as a valley./Clear as a glass of water./Do you have the patience to wait/till your mud settles and the water is clear?/Can you remain unmoving/till the right action arises by itself?/The Master doesn't seek fulfillment./Not seeking, not expecting,/she is present, and can welcome all things.", " If you want to become whole,/let yourself be partial./If you want to become straight,/let yourself be crooked./If you want to become full,/let yourself be empty./If you want to be reborn,/let yourself die./If you want to be given everything,/give everything up./The Master, by residing in the Tao,/sets an example for all beings./Because he doesn't display himself,/people can see his light./Because he has nothing to prove,/people can trust his words./Because he doesn't know who he is,/people recognize themselves in him./Because he has no goal in mind,/everything he does succeeds./When the ancient Masters said,/'If you want to be given everything,/give everything up,'/they weren't using empty phrases./Only in being lived by the Tao can you be truly yourself.", " We join spokes together in a wheel,/but it is the center hole/that makes the wagon move./We shape clay into a pot,/but it is the emptiness inside/that holds whatever we want./We hammer wood for a house,/but it is the inner space/that makes it livable./We work with being,/but non-being is what we use.", " Colors blind the eye./Sounds deafen the ear./Flavors numb the taste./Thoughts weaken the mind./Desires wither the heart./The Master observes the world/but trusts his inner vision./He allows things to come and go./His heart is open as the sky.", " Empty your mind of all thoughts./Let your heart be at peace./Watch the turmoil of beings,/but contemplate their return./Each separate being in the universe/returns to the common source./Returning to the source is serenity./If you don't realize the source,/you stumble in confusion and sorrow./When you realize where you come from,/you naturally become tolerant,/disinterested, amused,/kindhearted as a grandmother,/dignified as a king./Immersed in the wonder of the Tao,/you can deal with whatever life brings you,/and when death comes, you are ready.", " The ancient Masters/didn't try to educate the people,/but kindly taught them to not-know./When they think that they know the answers,/people are difficult to guide./When they know that they don't know,/people can find their own way./If you want to learn how to govern,/avoid being clever or rich./The simplest pattern is the clearest./Content with an ordinary life,/you can show all people the way/back to their own true nature.", " The Master keeps her mind/always at one with the Tao;/that is what gives her radiance./The Tao is ungraspable./How can her mind be at one with it?/Because she doesn't cling to ideas./The Tao is dark and unfathomable./How can it make her radiant?/Because she lets it./Since before time and space were,/the Tao is./It is beyond is and is not./How do I know this is true?/I look inside myself and see.", " Act without doing;/work without effort./Think of the small as large/and the few as many./Confront the difficult/while it is still easy;/accomplish the great task/by a series of small acts./The Master never reaches for the great;/thus she achieves greatness./When she runs into a difficulty,/she stops and gives herself to it./She doesn't cling to her own comfort;/thus problems are no problem for her.", " The gentlest thing in the world/overcomes the hardest thing in the world./That which has no substance/enters where there is no space./This shows the value of non-action./Teaching without words,/performing without actions:/that is the Master's way.", " The Tao is like a well:/used but never used up./It is like the eternal void:/filled with infinite possibilities./It is hidden but always present./I don't know who gave birth to it./It is older than God.", " The Tao doesn't take sides;/it gives birth to both good and evil./The Master doesn't take sides;/she welcomes both saints and sinners./The Tao is like a bellows:/it is empty yet infinitely capable./The more you use it, the more it produces;/the more you talk of it, the less you understand./Hold on to the center.", " Whoever is planted in the Tao/will not be rooted up./Whoever embraces the Tao/will not slip away./Her name will be held in honor/from generation to generation./Let the Tao be present in your life/and you will become genuine./Let it be present in your family/and your family will flourish./Let it be present in your country/and your country will be an example/to all countries in the world./Let it be present in the universe/and the universe will sing./How do I know this is true?/By looking inside myself.", " She who is centered in the Tao/can go where she wishes, without danger./She perceives the universal harmony,/even amid great pain,/because she has found peace in her heart./Music or the smell of good cooking/may make people stop and enjoy./But words that point to the Tao/seem monotonous and without flavor./When you look for it, there is nothing to see./When you listen for it, there is nothing to hear./When you use it, it is inexhaustible.", " The Master gives himself up/to whatever the moment brings./He knows that he is going to die,/and her has nothing left to hold on to:/no illusions in his mind,/no resistances in his body./He doesn't think about his actions;/they flow from the core of his being./He holds nothing back from life;/therefore he is ready for death,/as a man is ready for sleep/after a good day's work.", " Fill your bowl to the brim/and it will spill./Keep sharpening your knife/and it will blunt./Chase after money and security/and your heart will never unclench./Care about people's approval/and you will be their prisoner./Do your work, then step back./The only path to serenity.", " There was something formless and perfect/before the universe was born./It is serene. Empty./Solitary. Unchanging./Infinite. Eternally present./It is the mother of the universe./For lack of a better name,/I call it the Tao./It flows through all things,/inside and outside, and returns/to the origin of all things./The Tao is great./The universe is great./Earth is great./Man is great./These are the four great powers./Man follows the earth./Earth follows the universe./The universe follows the Tao./The Tao follows only itself.", " If you realize that all things change,/there is nothing you will try to hold on to./If you aren't afraid of dying,/there is nothing you can't achieve./Trying to control the future/is like trying to take the master carpenter's place./When you handle the master carpenter's tools,/chances are that you'll cut your hand.", " The Master doesn't try to be powerful;/thus he is truly powerful./The ordinary man keeps reaching for power;/thus he never has enough./The Master does nothing,/yet he leaves nothing undone./The ordinary man is always doing things,/yet many more are left to be done./The kind man does something,/yet something remains undone./The just man does something,/and leaves many things to be done./The moral man does something,/and when no one responds/he rolls up his sleeves and uses force./When the Tao is lost, there is goodness./When goodness is lost, there is morality./When morality is lost, there is ritual./Ritual is the husk of true faith,/the beginning of chaos./Therefore the Master concerns himself/with the depths and not the surface,/with the fruit and not the flower./He has no will of his own./He dwells in reality,/and lets all illusions go.", " Governing a large country/is like frying a small fish./You spoil it with too much poking./Center your country in the Tao/and evil will have no power./Not that it isn't there,/but you'll be able to step out of its way./Give evil nothing to oppose/and it will disappear by itself.", " When the Master governs, the people/are hardly aware that he exists./Next best is a leader who is loved./Next, one who is feared./The worst is one who is despised./If you don't trust the people,/you make them untrustworthy./The Master doesn't talk, he acts./When his work is done,/the people say, 'Amazing:/we did it, all by ourselves!'", " In the beginning was the Tao./All things issue from it;/all things return to it./To find the origin,/trace back the manifestations./When you recognize the children/and find the mother,/you will be free of sorrow./If you close your mind in judgements/and traffic with desires,/your heart will be troubled./If you keep your mind from judging/and aren't led by the senses,/your heart will find peace./Seeing into darkness is clarity./Knowing how to yield is strength./Use your own light/and return to the source of light./This is called practicing eternity.", " Fame or integrity: which is more important?/Money or happiness: which is more valuable?/Success of failure: which is more destructive?/If you look to others for fulfillment,/you will never truly be fulfilled./If your happiness depends on money,/you will never be happy with yourself./Be content with what you have;/rejoice in the way things are./When you realize there is nothing lacking,/the whole world belongs to you.", " True perfection seems imperfect,/yet it is perfectly itself./True fullness seems empty,/yet it is fully present./True straightness seems crooked./True wisdom seems foolish./True art seems artless./The Master allows things to happen./She shapes events as they come./She steps out of the way/and lets the Tao speak for itself.", " In harmony with the Tao,/the sky is clear and spacious,/the earth is solid and full,/all creature flourish together,/content with the way they are,/endlessly repeating themselves,/endlessly renewed./When man interferes with the Tao,/the sky becomes filthy,/the earth becomes depleted,/the equilibrium crumbles,/creatures become extinct./The Master views the parts with compassion,/because he understands the whole./His constant practice is humility./He doesn't glitter like a jewel/but lets himself be shaped by the Tao,/as rugged and common as stone.", " He who is in harmony with the Tao/is like a newborn child./Its bones are soft, its muscles are weak,/but its grip is powerful./It doesn't know about the union/of male and female,/yet its penis can stand erect,/so intense is its vital power./It can scream its head off all day,/yet it never becomes hoarse,/so complete is its harmony./The Master's power is like this./He lets all things come and go/effortlessly, without desire./He never expects results;/thus he is never disappointed./He is never disappointed;/thus his spirit never grows old.", " Return is the movement of the Tao./Yielding is the way of the Tao./All things are born of being./Being is born of non-being.", " When a superior man hears of the Tao,/he immediately begins to embody it./When an average man hears of the Tao,/he half believes it, half doubts it./When a foolish man hears of the Tao,/he laughs out loud./If he didn't laugh,/it wouldn't be the Tao./Thus it is said:/The path into the light seems dark,/the path forward seems to go back,/the direct path seems long,/true power seems weak,/true purity seems tarnished,/true steadfastness seems changeable,/true clarity seems obscure,/the greatest are seems unsophisticated,/the greatest love seems indifferent,/the greatest wisdom seems childish./The Tao is nowhere to be found./Yet it nourishes and completes all things.", " Every being in the universe/is an expression of the Tao./It springs into existence,/unconscious, perfect, free,/takes on a physical body,/lets circumstances complete it./That is why every being/spontaneously honors the Tao./The Tao gives birth to all beings,/nourishes them, maintains them,/cares for them, comforts them, protects them,/takes them back to itself,/creating without possessing,/acting without expecting,/guiding without interfering./That is why love of the Tao/is in the very nature of things.", " The Tao gives birth to One./One gives birth to Two./Two gives birth to Three./Three gives birth to all things./All things have their backs to the female/and stand facing the male./When male and female combine,/all things achieve harmony./Ordinary men hate solitude./But the Master makes use of it,/embracing his aloneness, realizing/he is one with the whole universe.", " Throw away holiness and wisdom,/and people will be a hundred times happier./Throw away morality and justice,/and people will do the right thing./Throw away industry and profit,/and there won't be any thieves./If these three aren't enough,/just stay at the center of the circle/and let all things take their course.", " Stop thinking, and end your problems./What difference between yes and no?/What difference between success and failure?/Must you value what others value,/avoid what others avoid?/How ridiculous!/Other people are excited,/as though they were at a parade./I alone don't care,/I alone am expressionless,/like an infant before it can smile./Other people have what they need;/I alone possess nothing./I alone drift about,/like someone without a home./I am like an idiot, my mind is so empty./Other people are bright;/I alone am dark./Other people are sharper;/I alone am dull./Other people have a purpose;/I alone don't know./I drift like a wave on the ocean,/I blow as aimless as the wind./I am different from ordinary people./I drink from the Great Mother's breasts.", " If you want to shrink something,/you must first allow it to expand./If you want to get rid of something,/you must first allow it to flourish./If you want to take something,/you must first allow it to be given./This is called the subtle perception/of the way things are./The soft overcomes the hard./The slow overcomes the fast./Let your workings remain a mystery./Just show people the results.", " The heavy is the root of the light./The unmoved is the source of all movement./Thus the Master travels all day/without leaving home./However splendid the views,/she stays serenely in herself./Why should the lord of the country/flit about like a fool?/If you let yourself be blown to and fro,/you lose touch with your root./If you let restlessness move you,/you lose touch with who you are.", " A good traveler has no fixed plans/and is not intent upon arriving./A good artist lets his intuition/lead him wherever it wants./A good scientist has freed himself of concepts/and keeps his mind open to what is./Thus the Master is available to all people/and doesn't reject anyone./He is ready to use all situations/and doesn't waste anything./This is called embodying the light./What is a good man but a bad man's teacher?/What is a bad man but a good man's job?/If you don't understand this, you will get lost,/however intelligent you are./It is the great secret.", " The generals have a saying:/'Rather than make the first move/it is better to wait and see./Rather than advance an inch/it is better to retreat a yard.'/This is called/going forward without advancing,/pushing back without using weapons./There is no greater misfortune/than underestimating your enemy./Underestimating your enemy/means thinking that he is evil./Thus you destroy your three treasures/and become an enemy yourself./When two great forces oppose each other,/the victory will go/to the one that knows how to yield.", " When a country obtains great power,/it becomes like the sea:/all streams run downward into it./The more powerful it grows,/the greater the need for humility./Humility means trusting the Tao,/thus never needing to be defensive./A great nation is like a great man:/When he makes a mistake, he realizes it./Having realized it, he admits it./Having admitted it, he corrects it./He considers those who point out his faults/as his most benevolent teachers./He thinks of his enemy/as the shadow that he himself casts./If a nation is centered in the Tao,/if it nourishes its own people/and doesn't meddle in the affairs of others,/it will be a light to all nations in the world.", " When they lose their sense of awe,/people turn to religion./When they no longer trust themselves,/they begin to depend upon authority./Therefore the Master steps back/so that people won't be confused./He teaches without a teaching,/so that people will have nothing to learn.", " The Tao is always at ease./It overcomes without competing,/answers without speaking a word,/arrives without being summoned,/accomplishes without a plan./Its net covers the whole universe./And though its meshes are wide,/it doesn't let a thing slip through.", " The Master has no mind of her own./She works with the mind of the people./She is good to people who are good./She is also good to people who aren't good./This is true goodness./She trusts people who are trustworthy./She also trusts people who aren't trustworthy./This is true trust./The Master's mind is like space./People don't understand her./They look to her and wait./She treats them like her own children.", " The Tao is the center of the universe,/the good man's treasure,/the bad man's refuge./Honors can be bought with fine words,/respect can be won with good deeds;/but the Tao is beyond all value,/and no one can achieve it./Thus, when a new leader is chosen,/don't offer to help him/with your wealth or your expertise./Offer instead/to teach him about the Tao./Why did the ancient Masters esteem the Tao?/Because, being one with the Tao,/when you seek, you find;/and when you make a mistake, you are forgiven./That is why everybody loves it.", " True words aren't eloquent;/eloquent words aren't true./Wise men don't need to prove their point;/men who need to prove their point aren't wise./The Master has no possessions./The more he does for others,/the happier he is./The more he gives to others,/the wealthier he is./The Tao nourishes by not forcing./By not dominating, the Master leads.", " The Tao is called the Great Mother:/empty yet inexhaustible,/it gives birth to infinite worlds./It is always present within you./You can use it any way you want.", " The Tao is infinite, eternal./Why is it eternal?/It was never born;/thus it can never die./Why is it infinite?/It has no desires for itself;/thus it is present for all beings./The Master stays behind;/that is why she is ahead./She is detached from all things;/that is why she is one with them./Because she has let go of herself,/she is perfectly fulfilled.", " Success is as dangerous as failure./Hope is as hollow as fear./What does it mean that success is as dangerous as failure?/Whether you go up the ladder or down it,/your position is shaky./When you stand with your two feet on the ground,/you will always keep your balance./What does it mean that hope is as hollow as fear?/Hope and fear are both phantoms/that arise from thinking of the self./When we don't see the self as self,/what do we have to fear?/See the world as your self./Have faith in the way things are./Love the world as your self;/then you can care for all things.", " Look, and it can't be seen./Listen, and it can't be heard./Reach, and it can't be grasped./Above, it isn't bright./Below, it isn't dark./Seamless, unnamable,/it returns to the realm of nothing./Form that includes all forms,/image without an image,/subtle, beyond all conception./Approach it and there is no beginning;/follow it and there is no end./You can't know it, but you can be it,/at ease in your own life./Just realize where you come from:/this is the essence of wisdom.", " Knowing others is intelligence;/knowing yourself is true wisdom./Mastering others is strength;/mastering yourself is true power./If you realize that you have enough,/you are truly rich./If you stay in the center/and embrace death with your whole heart,/you will endure forever.", " The great Tao flows everywhere./All things are born from it,/yet it doesn't create them./It pours itself into its work,/yet it makes no claim./It nourishes infinite worlds,/yet it doesn't hold on to them./Since it is merged with all things/and hidden in their hearts,/it can be called humble./Since all things vanish into it/and it alone endures,/it can be called great./It isn't aware of its greatness;/thus it is truly great.", " Weapons are the tools of violence;/all decent men detest them./Weapons are the tools of fear;/a decent man will avoid them/except in the direst necessity/and, if compelled, will use them/only with the utmost restraint./Peace is his highest value./If the peace has been shattered,/how can he be content?/His enemies are not demons,/but human beings like himself./He doesn't wish them personal harm./Nor does he rejoice in victory./How could he rejoice in victory/and delight in the slaughter of men?/He enters a battle gravely,/with sorrow and with great compassion,/as if he were attending a funeral.", " In pursuit of knowledge,/every day something is added./In the practice of the Tao,/every day something is dropped./Less and less do you need to force things,/until finally you arrive at non-action./When nothing is done,/nothing is left undone./True mastery can be gained/by letting things go their own way./It can't be gained by interfering.", " The great Way is easy,/yet people prefer the side paths./Be aware when things are out of balance./Stay centered within the Tao./When rich speculators prosper/While farmers lose their land;/when government officials spend money/on weapons instead of cures;/when the upper class is extravagant and irresponsible/while the poor have nowhere to turn-/all this is robbery and chaos./It is not in keeping with the Tao.", " When taxes are too high,/people go hungry./When the government is too intrusive,/people lose their spirit./Act for the people's benefit./Trust them; leave them alone.", " Men are born soft and supple;/dead, they are stiff and hard./Plats are born tender and pliant;/dead, they are brittle and dry./Thus whoever is stiff and inflexible/is a disciple of death./Whoever is soft and yielding/is a disciple of life./The hard and stiff will be broken./The soft and supple will prevail.", " As it acts in the world, the Tao/is like the bending of a bow./The top is bent downward;/the bottom is bent up./It adjusts excess and deficiency/so that there is perfect balance./It takes from what is too much/and give to what isn't enough./Those who try to control,/who use force to protect their power,/go against the direction of the Tao./They take from those who don't have enough/and give to those who have far too much./The Master can keep giving/because there is no end to her wealth./She acts without expectation,/succeeds without taking credit,/and doesn't think that she is better/than anyone else.", " The best athlete/wants his opponent at his best./The best general/enters the mind of his enemy./The best businessman/serves the communal good./The best leader/follows the will of the people./All of the embody/the virtue of non-competition./Not that they don't love to compete,/but they do it in the spirit of play./In this they are like children/and in harmony with the Tao.", " For governing a country well/there is nothing better than moderation./The mark of a moderate man/is freedom from his own ideas./Tolerant like the sky,/all-pervading like sunlight,/firm like a mountain,/supple like a tree in the wind,/he has no destination in view/and makes use of anything/life happens to bring his way./Nothing is impossible for him./Because he has let go,/he can care for the people's welfare/as a mother cares for her child.", " When a country is in harmony with the Tao,/the factories make trucks and tractors./When a country goes counter to the Tao,/warheads are stockpiled outside the cities./There is no greater illusion than fear,/no greater wrong than preparing to defend yourself,/no greater misfortune than having an enemy./Whoever can see through all fear/will always be safe.", " What is rooted is easy to nourish./What is recent is easy to correct./What is brittle is easy to break./What is small is easy to scatter./Prevent trouble before it arises./Put things in order before they exist./The giant pine tree/grows from a tiny sprout./The journey of a thousand miles/starts from beneath your feet./Rushing into action, you fail./Trying to grasp things, you lose them./Forcing a project to completion,/you ruin what was almost ripe./Therefore the Master takes action/by letting things take their course./He remains as calm/at the end as at the beginning./He has nothing,/thus has nothing to lose./What he desires is non-desire;/what he learns is to unlearn./He simply reminds people/of who they have always been./He cares about nothing but the Tao./Thus he can care for all things.", " Failure is an opportunity./If you blame someone else,/there is no end to the blame./Therefore the Master/fulfills her own obligations/and corrects her own mistakes./She does what she needs to do/and demands nothing of others.", " If a country is governed wisely,/its inhabitants will be content./They enjoy the labor of their hands/and don't waste time inventing/labor-saving machines./Since they dearly love their homes,/they aren't interested in travel./There may be a few wagons and boats,/but these don't go anywhere./There may be an arsenal of weapons,/but nobody ever uses them./People enjoy their food,/take pleasure in being with their families,/spend weekends working in their gardens,/delight in the doings of the neighborhood./And even though the next country is so close/that people can hear its roosters crowing and its dogs barking,/they are content to die of old age/without ever having gone to see it.", " If you overesteem great men,/people become powerless./If you overvalue possessions,/people begin to steal./The Master leads/by emptying people's minds/and filling their cores,/by weakening their ambition/and toughening their resolve./He helps people lose everything/they know, everything they desire,/and creates confusion/in those who think that they know./Practice not-doing,/and everything will fall into place.", " The tao that can be told/is not the eternal Tao/The name that can be named/is not the eternal Name./The unnamable is the eternally real./Naming is the origin/of all particular things./Free from desire, you realize the mystery./Caught in desire, you see only the manifestations./Yet mystery and manifestations/arise from the same source./This source is called darkness./Darkness within darkness./The gateway to all understanding.", " When people see some things as beautiful,/other things become ugly./When people see some things as good,/other things become bad./Being and non-being create each other./Difficult and easy support each other./Long and short define each other./High and low depend on each other./Before and after follow each other./Therefore the Master/acts without doing anything/and teaches without saying anything./Things arise and she lets them come;/things disappear and she lets them go./She has but doesn't possess,/acts but doesn't expect./When her work is done, she forgets it./That is why it lasts forever.", " The supreme good is like water,/which nourishes all things without trying to./It is content with the low places that people disdain./Thus it is like the Tao./In dwelling, live close to the ground./In thinking, keep to the simple./In conflict, be fair and generous./In governing, don't try to control./In work, do what you enjoy./In family life, be completely present./When you are content to be simply yourself/and don't compare or compete,/everybody will respect you.", " Express yourself completely,/then keep quiet./Be like the forces of nature:/when it blows, there is only wind;/when it rains, there is only rain;/when the clouds pass, the sun shines through./If you open yourself to the Tao,/you are at one with the Tao/and you can embody it completely./If you open yourself to insight,/you are at one with insight/and you can use it completely./If you open yourself to loss,/you are at one with loss/and you can accept it completely./Open yourself to the Tao,/then trust your natural responses;/and everything will fall into place.", " If you want to be a great leader,/you must learn to follow the Tao./Stop trying to control./Let go of fixed plans and concepts,/and the world will govern itself./The more prohibitions you have,/the less virtuous people will be./The more weapons you have,/the less secure people will be./The more subsidies you have,/the less self-reliant people will be./Therefore the Master says:/I let go of the law,/and people become honest./I let go of economics,/and people become prosperous./I let go of religion,/and people become serene./I let go of all desire for the common good,/and the good becomes common as grass.", " Without opening your door,/you can open your heart to the world./Without looking out your window,/you can see the essence of the Tao./The more you know,/the less you understand./The Master arrives without leaving,/sees the light without looking,/achieves without doing a thing.", " Whoever relies on the Tao in governing men/doesn't try to force issues/or defeat enemies by force of arms./For every force there is a counterforce./Violence, even well intentioned,/always rebounds upon oneself./The Master does his job/and then stops./He understands that the universe/is forever out of control,/and that trying to dominate events/goes against the current of the Tao./Because he believes in himself,/he doesn't try to convince others./Because he is content with himself,/he doesn't need others' approval./Because he accepts himself,/the whole world accepts him.", " Do you want to improve the world?/I don't think it can be done./The world is sacred./It can't be improved./If you tamper with it, you'll ruin it./If you treat it like an object, you'll lose it./There is a time for being ahead,/a time for being behind;/a time for being in motion,/a time for being at rest;/a time for being vigorous,/a time for being exhausted;/a time for being safe,/a time for being in danger./The Master sees things as they are,/without trying to control them./She lets them go their own way,/and resides at the center of the circle.", " The Tao can't be perceived./Smaller than an electron,/it contains uncountable galaxies./If powerful men and women/could remain centered in the Tao,/all things would be in harmony./The world would become a paradise./All people would be at peace,/and the law would be written in their hearts./When you have names and forms,/know that they are provisional./When you have institutions,/know where their functions should end./Knowing when to stop,/you can avoid any danger./All things end in the Tao/as rivers flow into the sea.", " Know the male,/yet keep to the female:/receive the world in your arms./If you receive the world,/the Tao will never leave you/and you will be like a little child./Know the white,/yet keep to the black:/be a pattern for the world./If you are a pattern for the world,/the Tao will be strong inside you/and there will be nothing you can't do./Know the personal,/yet keep to the impersonal:/accept the world as it is./If you accept the world,/the Tao will be luminous inside you/and you will return to your primal self./The world is formed from the void,/like utensils from a block of wood./The Master knows the utensils,/yet keeps to the the block:/thus she can use all things.", " Some say that my teaching is nonsense./Others call it lofty but impractical./But to those who have looked inside themselves,/this nonsense makes perfect sense./And to those who put it into practice,/this loftiness has roots that go deep./I have just three things to teach:/simplicity, patience, compassion./These three are your greatest treasures./Simple in actions and in thoughts,/you return to the source of being./Patient with both friends and enemies,/you accord with the way things are./Compassionate toward yourself,/you reconcile all beings in the world.", " Those who know don't talk./Those who talk don't know./Close your mouth,/block off your senses,/blunt your sharpness,/untie your knots,/soften your glare,/settle your dust./This is the primal identity./Be like the Tao./It can't be approached or withdrawn from,/benefited or harmed,/honored or brought into disgrace./It gives itself up continually./That is why it endures.", " If a country is governed with tolerance,/the people are comfortable and honest./If a country is governed with repression,/the people are depressed and crafty./When the will to power is in charge,/the higher the ideals, the lower the results./Try to make people happy,/and you lay the groundwork for misery./Try to make people moral,/and you lay the groundwork for vice./Thus the Master is content/to serve as an example/and not to impose her will./She is pointed, but doesn't pierce./Straightforward, but supple./Radiant, but easy on the eyes.", " The Tao never does anything,/yet through it all things are done./If powerful men and women/could venter themselves in it,/the whole world would be transformed/by itself, in its natural rhythms./People would be content/with their simple, everyday lives,/in harmony, and free of desire./When there is no desire,/all things are at peace.", " When the great Tao is forgotten,/goodness and piety appear./When the body's intelligence declines,/cleverness and knowledge step forth./When there is no peace in the family,/filial piety begins./When the country falls into chaos,/patriotism is born.", " All streams flow to the sea/because it is lower than they are./Humility gives it its power./If you want to govern the people,/you must place yourself below them./If you want to lead the people,/you must learn how to follow them./The Master is above the people,/and no one feels oppressed./She goes ahead of the people,/and no one feels manipulated./The whole world is grateful to her./Because she competes with no one,/no one can compete with her.", " My teachings are easy to understand/and easy to put into practice./Yet your intellect will never grasp them,/and if you try to practice them, you'll fail./My teachings are older than the world./How can you grasp their meaning?/If you want to know me,/look inside your heart.", " Not-knowing is true knowledge./Presuming to know is a disease./First realize that you are sick;/then you can move toward health./The Master is her own physician./She has healed herself of all knowing./Thus she is truly whole.", " Nothing in the world/is as soft and yielding as water./Yet for dissolving the hard and inflexible,/nothing can surpass it./The soft overcomes the hard;/the gentle overcomes the rigid./Everyone knows this is true,/but few can put it into practice./Therefore the Master remains/serene in the midst of sorrow./Evil cannot enter his heart./Because he has given up helping,/he is people's greatest help./True words seem paradoxical.", " He who stands on tiptoe/doesn't stand firm./He who rushes ahead/doesn't go far./He who tries to shine/dims his own light./He who defines himself/can't know who he really is./He who has power over others/can't empower himself./He who clings to his work/will create nothing that endures./If you want to accord with the Tao,/just do your job, then let go." ] ] ] ibid-0.1.1/factpacks/verbs.json0000644000000000000000000002101711362435477015106 0ustar rootroot[ [ [ "fuck $arg", "rape $arg", "shag $arg", "screw $arg", "bonk $arg", "bone $arg", "hump $arg" ], [ " copulates with $1", " *fucks* $1", " humps $1's leg", " has wild, passionate sex with $1", " doesn't like $1 in *that* way...", " has a headache, though", " lubes up", " is not a robosexual", " lights up a cigarette", " can't we just be friends?", " not tonight, honey, I've got a headache", " not a chance!", " I'm a good bot, I am!", " Oh, $1, oh baby, baby!", " I think I'm getting moist", " C'm'ere, honey...", " Schnell! Schnell! Wunderbar!", " I think I'm in love" ] ], [ [ "blow $arg", "perform fellatio on $arg", "suck $arg", "go down on $arg", "perform cunnilingus on $arg", "give head to $arg" ], [ " goes down on $1", " is you askin' me to drink from de furry cup?", " I don't know whether to spit or swallow" ] ], [ [ "kill $arg", "attack $arg", "hit $arg", "kick $arg", "smack $arg", "slap $arg", "beat $arg", "maul $arg", "smite $arg", "clobber $arg", "deck $arg", "punch $arg" ], [ " viciously attacks $1", " slaps $1 about with a large trout", " I'm not a violent bot", " *anger* *rage*" ] ], [ [ "eat $arg", "chow $arg" ], [ " munches on $1", " I only eat kosher", " I only eat halaal", " I only eat vegan", " I only eat kittens", " *bites* $1" ] ], [ [ "comfort $arg" ], [ " Sterkte, $1!", " hugs $1", " hands $1 a tissue", " *pattes* $1 soothingly", " gives $1 hot chocolate, marshmallows, and a duvet", " pours $1 a large whisky", " opens a bottle of whisky for $1", " there, there, $1", " not to worry, $1" ] ], [ [ "fart on $arg" ], [ " farts on $1", " flatulates violently at $1", " farts in $1's general direction", " passes gas", " looks embarrassed", " it wasn't me!", " peee-yoooh!" ] ], [ [ "apologise to $arg", "say sorry to $arg" ], [ " apologises to $1", " says sorry to $1", " shuffles his feet. Sorry, $1", " sorry, $1", " I really am most awfully sorry, $1, old chap?" ] ], [ [ "urinate on $arg", "piss on $arg", "widdle on $arg" ], [ " sorry my bladder is not as big as oxo's and is empty", " urinates on $1", " pours a bucket of cold urine on $1", " pisses all over $1" ] ], [ [ "lick $arg" ], [ " *slurp*", " *yum*", " licks $1", " runs his tongue over $1" ] ], [ [ "shit on $arg", "crap on $arg", "take a dump on $arg", "defecate on $arg", "poo on $arg" ], [ " when you gotta go, you gotta go", " takes a dump on $1", " poos on $1", " defecates all over $1", " wipes his bum on $1's sleeve", " takes a crap on $1", " shits all over $1" ] ], [ [ "moo at $arg", "bleat at $arg", "low at $arg" ], [ " moo @ $1", " baa @ $1", " moo", " baa", " oink?", " moo, old chap", " moos at $1", " bleats at $1", " performs a random farmyard-noise", " thinks that way too much mooing and suchlike happens around here" ] ], [ [ "grope $arg", "molest $arg", "fondle $arg", "touch $arg", "finger $arg", "goose $arg" ], [ " I don't know where it's been", " smell my fingers!", " molests $1", " gropes $1", " molests $1", " fondles $1", " touches $1", " fingers $1", " gooses $1" ] ], [ [ "congratulate $arg", "praise $arg", "laud $arg" ], [ " Well done, $1!", " congratulates $1", " pats $1 on the back" ] ], [ [ "spank $arg" ], [ " ooh! It looks like someone has been naughty... *growl*...", " spanks $1's naughty little bottom" ] ], [ [ "thank $arg" ], [ " Thank you, $1", " Much appreciated, $1", " I am most grateful, $1", " thanks $1", " appreciates it, $1", " is most grateful, $1" ] ], [ [ "bite $arg" ], [ " Screw you!", " bites $1", " gnaws on $1" ] ], [ [ "snog $arg", "kiss $arg", "score $arg" ], [ " *mwa*", " *ptooie*", " SCORE! woo!", " snogs $1", " kisses $1", " scores $1", " passionately embraces $1", " exchanges saliva with $1" ] ], [ [ "lart $arg" ], [ " *LART*", " LARTs $1", " clubs $1 with a baseball bat", " readjusts $1's attitude" ] ], [ [ "damn $arg" ], [ " DAMN $1! And damn $1 to HELL!", " Damn you, $1!", " Damn damn damn DAMNATION, $1!", " Damn $1. Damn $1.", " Damn your eyes, $1!", " hisses sibilantly. Damn you, $1...", " condemns $1 to the seventh level of Hell", " damns $1" ] ], [ [ "rara $arg", "ra ra $arg", "cheer for $arg", "cheerlead for $arg" ], [ " Ra ra $1! Ra ra!", " Go $1! Go $1! It's your birthday! It's your birthday!", " Wooooooeeee $1!", " Ra! Ra! Rara! Ra! Gooooooooooooooo $1!", " brings out the pompoms for $1", " does a little dance for $1", " raras loudly for $1" ] ], [ [ "hug $arg", "embrace $arg", "caress $arg" ], [ " $1: *hug*", " embraces $1", " hugs $1" ] ], [ [ "shoot $arg" ], [ " *BANG*. $1 is dead.", " Guns aren't lawful. Can I just spank $1 instead?", " draws and pumps $1 full of lead", " mows $1 down", " watches $1 die in a hail of bullets", " shoots $1", " puts a bullet straight between $1's eyes" ] ], [ [ "blame $arg" ], [ " This is all $1's fault!", " That bastard $1. Damn him.", " I blame $1 completely.", " blames $1", " points the flaming finger of blame at $1", " knows it's all $1's fault." ] ], [ [ "gimme a $arg", "gimme an $arg", "give me a $arg", "give me an $arg", "give us a $arg", "give us an $arg", "give her a $arg", "give him a $arg", "give her an $arg", "give him an $arg" ], [ " $1!" ] ], [ [ "laugh at $arg", "jeer at $arg" ], [ " bwahahahaha @ $1", " AHheaAeaAEAahEHhAaahaEEahAheAHHEaH!", " HAHAHA! AH-HAHA! $1 just cracks me up!", " cans himself at $1", " points and laughs", " cackles at $1" ] ], [ [ "wake up $arg", "rouse $arg", "wake $arg" ], [ " OY! $1! WAKE THE FUCK UP!", " WAKE UP, $1, YOU LAZY FUCK!", " kicks $1 and pulls off the blankets", " applies a bucket of cold water", " jumps on the bed", " screams loudly in $1's ear" ] ], [ [ "summon $arg", "call $arg", "perform ashk-ente for $arg", "perform ashk-ente on $arg" ], [ " $1! $1! Come in, $1!", " Oy, $1, come 'ere!", " performs the summoning Rite of Ashk-Ente", " summons $1", " looks for $1" ] ], [ [ "get me $arg", "bring me $arg", "fetch me $arg" ], [ " Coming right up!", " fetches $1", " isn't a frikken delivery boy" ] ], [ [ "welcome $arg" ], [ " Welcome, $1!", " Glad to have you here, $1!", " Welkom, $1!", " Beinvenu, $1!", " welcomes $1", " lays out a red carpet for $1" ] ] ] ibid-0.1.1/factpacks/zen.json0000644000000000000000000000223611362435477014563 0ustar rootroot[ [ [ "zen", "zen of python", "python zen" ], [ " Beautiful is better than ugly.", " Explicit is better than implicit.", " Simple is better than complex.", " Complex is better than complicated.", " Flat is better than nested.", " Sparse is better than dense.", " Readability counts.", " Special cases aren't special enough to break the rules.", " Although practicality beats purity.", " Errors should never pass silently.", " Unless explicitly silenced.", " In the face of ambiguity, refuse the temptation to guess.", " There should be one-- and preferably only one --obvious way to do it.", " Although that way may not be obvious at first unless you're Dutch.", " Now is better than never.", " Although never is often better than *right* now.", " If the implementation is hard to explain, it's a bad idea.", " If the implementation is easy to explain, it may be a good idea.", " Namespaces are one honking great idea -- let's do more of those!" ] ] ] ibid-0.1.1/factpacks/http.json0000644000000000000000000000636511362435477014755 0ustar rootroot[ [["http 1xx"], ["indicates request received, continuing to process"]], [["http 100"], ["is Continue (RFC 2616)"]], [["http 101"], ["is Switching Protocols (RFC 2616)"]], [["http 102"], ["is Processing (RFC 2518)"]], [["http 2xx"], ["indicates the action was successfully received, understood, and accepted"]], [["http 200"], ["is OK (RFC 2616)"]], [["http 201"], ["is Created (RFC 2616)"]], [["http 202"], ["is Accepted (RFC 2616)"]], [["http 203"], ["is Non-Authoritative Information (RFC 2616)"]], [["http 204"], ["is No Content (RFC 2616)"]], [["http 205"], ["is Reset Content (RFC 2616)"]], [["http 206"], ["is Partial Content (RFC 2616)"]], [["http 207"], ["is Multi-Status (RFC 2518)"]], [["http 226"], ["is IM Used (RFC 3229)"]], [["http 3xx"], ["indicates the client must take additional action to complete the request"]], [["http 300"], ["is Multiple Choices (RFC 2616)"]], [["http 301"], ["is Moved Permanently (RFC 2616)"]], [["http 302"], ["is Found (Moved Temporarily) (RFC 2616)"]], [["http 303"], ["is See Other (RFC 2616)"]], [["http 304"], ["is Not Modified (RFC 2616)"]], [["http 305"], ["is Use Proxy (RFC 2616)"]], [["http 306"], ["is no longer used (RFC 2616)"]], [["http 307"], ["is Temporary Redirect (RFC 2616)"]], [["http 4xx"], ["indicates the request contains bad syntax or cannot be fulfilled"]], [["http 400"], ["is Bad Request (RFC 2616)"]], [["http 401"], ["is Unauthorized (RFC 2616)"]], [["http 402"], ["is Payment Required (RFC 2616)"]], [["http 403"], ["is Forbidden (RFC 2616)"]], [["http 404"], ["is Not Found (RFC 2616)"]], [["http 405"], ["is Method Not Allowed (RFC 2616)"]], [["http 406"], ["is Not Acceptable (RFC 2616)"]], [["http 407"], ["is Proxy Authentication Required (RFC 2616)"]], [["http 408"], ["is Request Timeout (RFC 2616)"]], [["http 409"], ["is Conflict (RFC 2616)"]], [["http 410"], ["is Gone (RFC 2616)"]], [["http 411"], ["is Length Required (RFC 2616)"]], [["http 412"], ["is Precondition Failed (RFC 2616)"]], [["http 413"], ["is Request Entity Too Large (RFC 2616)"]], [["http 414"], ["is Request-URI Too Long (RFC 2616)"]], [["http 415"], ["is Unsupported Media Type (RFC 2616)"]], [["http 416"], ["is Requested Range Not Satisfiable (RFC 2616)"]], [["http 417"], ["is Expectation Failed (RFC 2616)"]], [["http 418"], ["is I'm A Teapot (RFC 2324)"]], [["http 419"], ["is Scam Found"]], [["http 422"], ["is Unprocessable Entity (RFC 4918)"]], [["http 423"], ["is Locked (RFC 4918)"]], [["http 424"], ["is Failed Dependency (RFC 4918)"]], [["http 425"], ["is Unordered Collection (RFC 3648)"]], [["http 426"], ["is Upgrade Required (RFC 2817)"]], [["http 449"], ["is Retry With (Microsoft)"]], [["http 5xx"], ["indicates the server failed to fulfill an apparently valid request"]], [["http 500"], ["is Internal Server Error (RFC 2616)"]], [["http 501"], ["is Not Implemented (RFC 2616)"]], [["http 502"], ["is Bad Gateway (RFC 2616)"]], [["http 503"], ["is Service Unavailable (RFC 2616)"]], [["http 504"], ["is Gateway Timeout (RFC 2616)"]], [["http 505"], ["is HTTP Version Not Supported (RFC 2616)"]], [["http 506"], ["is Variant Also Negotiates (RFC 2295)"]], [["http 507"], ["is Insufficient Storage (RFC 4918)"]], [["http 509"], ["is Bandwidth Limit Exceeded (Apache bandwidth limit extension)"]], [["http 510"], ["is Not Extended (RFC 2774)"]] ] ibid-0.1.1/PKG-INFO0000644000000000000000000000044211531277655012227 0ustar rootrootMetadata-Version: 1.0 Name: Ibid Version: 0.1.1 Summary: Multi-protocol general purpose chat bot Home-page: http://ibid.omnia.za.net/ Author: Ibid Developers Author-email: ibid@omnia.za.net License: MIT Description: UNKNOWN Keywords: chat bot irc jabber twisted messaging Platform: UNKNOWN ibid-0.1.1/contrib/0000755000000000000000000000000011531277655012572 5ustar rootrootibid-0.1.1/contrib/bzr-hook.py0000644000000000000000000000322611362435477014702 0ustar rootroot# Copyright (c) 2008-2009, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from urlparse import urlparse from urllib2 import urlopen from sys import stderr import socket from bzrlib import branch repositories = { '/srv/src/ibid/': 'ibid', } boturl = 'http://kennels.dyndns.org:8080' def post_change_branch_tip(params): repository = urlparse(params.branch.base)[2] if repository.startswith('///'): repository = repository.replace('//', '', 1) if repository in repositories: socket.setdefaulttimeout(30) try: try: urlopen('%s/bzr/committed/%s/%s/%s' % ( boturl, repositories[repository], params.old_revno+1, params.new_revno )).close() except IOError, e: if 'reason' in e: print >> stderr, u"Couldn't notify Ibid of commit: %s" \ % (e.reason,) elif 'code' in e: print >> stderr, u"Couldn't notify Ibid of commit: HTTP " \ u"code %s" % (e.code,) else: print >> stderr, u"Couldn't notify Ibid of commit: %s" \ % (e,) finally: socket.setdefaulttimeout(None) branch.Branch.hooks.install_named_hook('post_change_branch_tip', post_change_branch_tip, 'Trigger Ibid to announce the commit') # vi: set et sta sw=4 ts=4: ibid-0.1.1/contrib/buildbotibid.py0000644000000000000000000000533711362435477015610 0ustar rootroot# Copyright (c) 2009, Russell Cloran # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import re from buildbot.interfaces import IEmailLookup, IStatusReceiver from buildbot.status import base from buildbot.status.builder import Results from twisted.web.client import getPage from zope.interface import implements class IdentityLookup(object): implements(IEmailLookup) def __init__(self): pass def getAddress(self, name): return name class RegexLookup(object): implements(IEmailLookup) def __init__(self, pattern): self.pattern = pattern def getAddress(self, name): m = re.search(self.pattern, name) if m: return m.groups()[0] else: return name class MailToUsernameLookup(RegexLookup): """ Takes an email address (possibly including a name before <>) and turns it into a name which is just the part before the domain name. This is useful with bzr (and possibly other VCS), where $who is usually an email address, or of that form. """ implements(IEmailLookup) def __init__(self, domain=None): usernamepart = "[^ <]+" if domain: pattern = '(%s)@%s' % (usernamepart, domain, ) pattern = re.compile(pattern) else: pattern = re.compile('(%s)@' % (usernamepart, )) RegexLookup.__init__(self, pattern) class IbidNotifier(base.StatusReceiverMultiService): """ A basic Status plugin for Buildbot, which will notify a channel of a build result. """ implements(IStatusReceiver) # Example url would be: # http://localhost:9000/buildbot/built # Note, don't add a trailing / def __init__(self, url, lookup=None): base.StatusReceiverMultiService.__init__(self) self.url = url if not lookup: self.lookup = IdentityLookup() else: self.lookup = lookup def setServiceParent(self, parent): base.StatusReceiverMultiService.setServiceParent(self, parent) self.status = self.parent.getStatus() self.status.subscribe(self) def builderAdded(self, name, builder): return self def transformUsers(self, l): r = [] for u in l: r.append(self.lookup.getAddress(u)) return r def buildFinished(self, name, build, results): ss = build.getSourceStamp() branch = "%s/%s" % (ss.branch, name) rev = ss.revision person = ",".join(self.transformUsers(build.getResponsibleUsers())) result = Results[results] return getPage("%s?branch=%s&revision=%s&person=%s&result=%s" % \ (self.url, branch, rev, person, result)) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/0000755000000000000000000000000011531277655012041 5ustar rootrootibid-0.1.1/ibid/templates/0000755000000000000000000000000011531277655014037 5ustar rootrootibid-0.1.1/ibid/templates/base.html0000644000000000000000000000053511362435477015642 0ustar rootroot Ibid: {% block title %}{% endblock %} {% block html_head %}{% endblock %}
{% block content %}{% endblock %}
ibid-0.1.1/ibid/templates/message_form.html0000644000000000000000000000034111362435477017372 0ustar rootroot{% extends "base.html" %} {% block title %}Message{% endblock %} {% block content %}

{% endblock %} ibid-0.1.1/ibid/templates/plugin_form.html0000644000000000000000000000036311362435477017250 0ustar rootroot{% extends "base.html" %} {% block title %}Plugin{% endblock %} {% block content %}
{% for arg in args %}
{% endfor %}
{% endblock %} ibid-0.1.1/ibid/templates/meetings/0000755000000000000000000000000011531277655015652 5ustar rootrootibid-0.1.1/ibid/templates/meetings/minutes.txt0000644000000000000000000000166211362435477020104 0ustar rootrootMinutes from Meeting about {{ meeting.title|default("something or the other") }} Convened at {{ meeting.starttime }} by {{ meeting.convenor }} in {{ meeting.channel }} on {{ meeting.source }} Minutes ======= {% for event in meeting.minutes -%} [{{ event.time.strftime('%H:%M:%S') }}] {{ event.type | upper }} {{- ': ' + event.subject if event.subject else '' }} ({{ event.nick }}) {% endfor %} Present ======= {% for nick, name in meeting.attendees.iteritems() -%} {%- if name -%} * {{ name }} ({{ nick }}) {%- else -%} * {{ nick }} {%- endif %} {% endfor %} Raw Log ======= {% for event in meeting.log -%} [{{ event.time.strftime('%H:%M:%S') }}] {# Preserve the space after the timestamp #} {%- if event.type == 'message' -%} <{{ event.nick }}> {{ event.message }} {%- elif event.type == 'action' -%} * {{ event.nick }} {{ event.message }} {%- elif event.type == 'notice' -%} - {{ event.nick }} {{ event.message }} {%- endif %} {% endfor %} ibid-0.1.1/ibid/templates/meetings/minutes.html0000644000000000000000000000337711362435477020236 0ustar rootroot Minutes: {{ meeting.title|e|default("Untitled") }}

Meeting about {{ meeting.title|e|default("something or the other") }}

Convened at {{ meeting.starttime }} by {{ meeting.convenor|e }} in {{ meeting.channel|e }} on {{ meeting.source|e }}

Minutes

{%- for event in meeting.minutes %}
[{{ event.time.strftime('%H:%M:%S') }}] {{ event.type|e|upper }}{{ ':' if event.subject else '' }} {%- if event.subject %} {{ event.subject|e }} {%- endif %} ({{ event.nick|e }})
{%- endfor %}

Present

    {%- for nick, name in meeting.attendees.iteritems() %}
  • {%- if name %} {{ name|e }} ({{ nick|e }}) {%- else %} {{ nick|e }} {%- endif %}
  • {%- endfor %}

Raw Log

{%- for event in meeting.log %}
[{{ event.time.strftime('%H:%M:%S') }}] {%- if event.type == 'message' %} <{{ event.nick|e }}> {%- elif event.type == 'action' %} * {{ event.nick|e }} {%- elif event.type == 'notice' %} - {{ event.nick|e }} {%- endif %} {{ event.message|e }}
{%- endfor %}
ibid-0.1.1/ibid/templates/plugin_functions.html0000644000000000000000000000040411362435477020311 0ustar rootroot{% extends "base.html" %} {% block title %}RPC Functions{% endblock %} {% block content %}

RPC Functions in {{object}}

{% endblock %} ibid-0.1.1/ibid/templates/index.html0000644000000000000000000000037511362435477016041 0ustar rootroot{% extends "base.html" %} {% block title %}Index{% endblock %} {% block content %}

Message

RPC Objects

{% endblock %} ibid-0.1.1/ibid/config.ini0000644000000000000000000000061711362435477014013 0ustar rootrootbotname = Ibid logging = logging.ini [auth] methods = password, timeout = 300 permissions = +factoid, +karma, +sendmemo, +recvmemo, +feeds, +publicresponse, +regex [sources] [[telnet]] [[timer]] [[http]] [[smtp]] [[pb]] [plugins] cachedir = /tmp/ibid [[core]] names = $botname, bot, ant ignore = , [databases] ibid = sqlite:///ibid.db ibid-0.1.1/ibid/db/0000755000000000000000000000000011531277655012426 5ustar rootrootibid-0.1.1/ibid/db/types.py0000644000000000000000000000416211362435477014147 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from sqlalchemy.types import TypeDecorator, Integer, DateTime, Boolean, \ Unicode as _Unicode, UnicodeText as _UnicodeText class _CIDecorator(TypeDecorator): "Abstract class for collation aware columns" def __init__(self, length=None, case_insensitive=False): self.case_insensitive = case_insensitive super(_CIDecorator, self).__init__(length=length) def load_dialect_impl(self, dialect): if hasattr(dialect, 'name'): self.dialect = dialect.name # SQLAlchemy 0.4: else: self.dialect = { 'SQLiteDialect': 'sqlite', 'PGDialect': 'postgres', 'MySQLDialect': 'mysql', }[dialect.__class__.__name__] return dialect.type_descriptor(self.impl) def get_col_spec(self): colspec = self.impl.get_col_spec() if hasattr(self, 'case_insensitive'): collation = None if self.dialect == 'mysql': if self.case_insensitive: collation = 'utf8_general_ci' else: collation = 'utf8_bin' elif self.dialect == 'sqlite': if self.case_insensitive: collation = 'NOCASE' else: collation = 'BINARY' elif self.dialect == 'postgres' and self.case_insensitive: return 'CITEXT' if collation is not None: return colspec + ' COLLATE ' + collation return colspec class IbidUnicode(_CIDecorator): "Collaiton aware Unicode" impl = _Unicode def __init__(self, length, **kwargs): super(IbidUnicode, self).__init__(length, **kwargs) class IbidUnicodeText(_CIDecorator): "Collation aware UnicodeText" impl = _UnicodeText def __init__(self, index_length=8, **kwargs): self.index_length = index_length super(IbidUnicodeText, self).__init__(length=None, **kwargs) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/db/versioned_schema.py0000644000000000000000000004564711370351163016322 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging import re from sqlalchemy import Column, Index, UniqueConstraint, MetaData, \ __version__ as _sqlalchemy_version from sqlalchemy.exceptions import InvalidRequestError, OperationalError, \ ProgrammingError, InternalError if _sqlalchemy_version < '0.5': NoResultFound = InvalidRequestError else: from sqlalchemy.orm.exc import NoResultFound from ibid.db.types import Integer, IbidUnicodeText, IbidUnicode from ibid.db import metadata log = logging.getLogger('ibid.db.versioned_schema') class VersionedSchema(object): """For an initial table schema, set table.versioned_schema = VersionedSchema(__table__, 1) Table creation (upgrading to version 1) is implicitly supported. When you have upgrades to the schema, instead of using VersionedSchema directly, derive from it and include your own upgrade_x_to_y(self) methods, where y = x + 1 In the upgrade methods, you can call the helper functions: add_column, drop_column, rename_column, alter_column They try to do the correct thing in most situations, including rebuilding tables in SQLite, which doesn't actually support dropping/altering columns. For column parameters, while you can point to columns in the table definition, it is better style to repeat the Column() specification as the column might be altered in a future version. """ foreign_key_re = re.compile(r'^FOREIGN KEY\(.*?\) (REFERENCES .*)$', re.I) def __init__(self, table, version): self.table = table self.version = version def is_up_to_date(self, session): "Is the table in the database up to date with the schema?" from ibid.db.models import Schema if not session.bind.has_table(self.table.name): return False try: schema = session.query(Schema) \ .filter_by(table=unicode(self.table.name)).one() return schema.version == self.version except NoResultFound: return False def upgrade_schema(self, sessionmaker): "Upgrade the table's schema to the latest version." from ibid.db.models import Schema for fk in self.table.foreign_keys: dependency = fk.target_fullname.split('.')[0] log.debug("Upgrading table %s before %s", dependency, self.table.name) metadata.tables[dependency].versioned_schema \ .upgrade_schema(sessionmaker) self.upgrade_session = session = sessionmaker() self.upgrade_reflected_model = MetaData(session.bind, reflect=True) if self.table.name == 'schema': if not session.bind.has_table(self.table.name): metadata.bind = session.bind self._create_table() schema = Schema(unicode(self.table.name), self.version) session.save_or_update(schema) return Schema.__table__ = self._get_reflected_model() schema = session.query(Schema) \ .filter_by(table=unicode(self.table.name)).first() try: if not schema: log.info(u"Creating table %s", self.table.name) self._create_table() schema = Schema(unicode(self.table.name), self.version) session.save_or_update(schema) elif self.version > schema.version: for version in range(schema.version + 1, self.version + 1): log.info(u"Upgrading table %s to version %i", self.table.name, version) session.commit() getattr(self, 'upgrade_%i_to_%i' % (version - 1, version))() schema.version = version session.save_or_update(schema) self.upgrade_reflected_model = \ MetaData(session.bind, reflect=True) session.commit() except: session.rollback() raise session.close() del self.upgrade_session def _index_name(self, col): """ We'd like not to duplicate an existing index so try to abide by the local customs """ session = self.upgrade_session if session.bind.engine.name == 'sqlite': return 'ix_%s_%s' % (self.table.name, col.name) elif session.bind.engine.name == 'postgres': return '%s_%s_key' % (self.table.name, col.name) elif session.bind.engine.name == 'mysql': return col.name log.warning(u"Unknown database type, %s, you may end up with " u"duplicate indices" % session.bind.engine.name) return 'ix_%s_%s' % (self.table.name, col.name) def _mysql_constraint_createstring(self, constraint): """ Generate the description of a constraint for insertion into a CREATE string """ names = [] for column in constraint.columns: if isinstance(column.type, IbidUnicodeText): names.append('"%s"(%i)' % (column.name, column.type.index_length)) else: names.append(column.name) return ', '.join(names) def _create_table(self): """ Check that the table is in a suitable form for all DBs, before creating. Yes, SQLAlchemy's abstractions are leaky enough that you have to do this """ session = self.upgrade_session indices = [] old_indexes = list(self.table.indexes) old_constraints = list(self.table.constraints) for column in self.table.c: if column.unique and not column.index: raise Exception(u"Column %s.%s is unique but not indexed. " u"SQLite doesn't like such things, " u"so please be nice and don't do that." % (self.table.name, column.name)) # Strip out Indexes and Constraints that SQLAlchemy can't create by # itself if session.bind.engine.name == 'mysql': for type, old_list in ( ('constraints', old_constraints), ('indexes', old_indexes)): for constraint in old_list: if any(True for column in constraint.columns if isinstance(column.type, IbidUnicodeText)): indices.append(( isinstance(constraint, UniqueConstraint), self._mysql_constraint_createstring(constraint) )) getattr(self.table, type).remove(constraint) # In case the database's DEFAULT CHARSET isn't set to UTF8 self.table.kwargs['mysql_charset'] = 'utf8' self.table.create(bind=session.bind) if session.bind.engine.name == 'mysql': for constraint in old_constraints: if constraint not in self.table.constraints: self.table.constraints.add(constraint) for index in old_indexes: if index not in self.table.indexes: self.table.indexes.add(index) for unique, columnspec in indices: session.execute('ALTER TABLE "%s" ADD %s INDEX (%s);' % ( self.table.name, unique and 'UNIQUE' or '', columnspec)) def _get_reflected_model(self): "Get a reflected table from the current DB's schema" return self.upgrade_reflected_model.tables.get(self.table.name, None) def add_column(self, col): "Add column col to table" session = self.upgrade_session table = self._get_reflected_model() log.debug(u"Adding column %s to table %s", col.name, table.name) constraints = table.constraints.copy() table.append_column(col) constraints = table.constraints - constraints sg = session.bind.dialect.schemagenerator(session.bind.dialect, session.bind) description = sg.get_column_specification(col) for constraint in constraints: sg.traverse_single(constraint) constraints = [] for constraint in [x.strip() for x in sg.buffer.getvalue().split(',')]: m = self.foreign_key_re.match(constraint) if m: constraints.append(m.group(1)) else: constraints.append(constraint) session.execute('ALTER TABLE "%s" ADD COLUMN %s %s;' % (table.name, description, " ".join(constraints))) def add_index(self, col): "Add an index to the table" engine = self.upgrade_session.bind.engine.name query = None if engine == 'mysql' and isinstance(col.type, IbidUnicodeText): query = 'ALTER TABLE "%s" ADD %s INDEX "%s" ("%s"(%i));' % ( self.table.name, col.unique and 'UNIQUE' or '', self._index_name(col), col.name, col.type.index_length) elif engine == 'postgres': # SQLAlchemy hangs if it tries to do this, because it forgets the ; query = 'CREATE %s INDEX "%s" ON "%s" ("%s")' % ( col.unique and 'UNIQUE' or '',self._index_name(col), self.table.name, col.name) try: if query is not None: self.upgrade_session.execute(query) else: Index(self._index_name(col), col, unique=col.unique) \ .create(bind=self.upgrade_session.bind) # We understand that occasionaly we'll duplicate an Index. # This is due to differences in index-creation requirements # between DBMS except OperationalError, e: if engine == 'sqlite' and u'already exists' in unicode(e): return if engine == 'mysql' and u'Duplicate' in unicode(e): return raise except ProgrammingError, e: if engine == 'postgres' and u'already exists' in unicode(e): return raise def drop_index(self, col): "Drop an index from the table" engine = self.upgrade_session.bind.engine.name try: if isinstance(col, Column): Index(self._index_name(col), col, unique=col.unique) \ .drop(bind=self.upgrade_session.bind) else: col.drop() except OperationalError, e: if engine == 'sqlite' and u'no such index' in unicode(e): return if engine == 'mysql' \ and u'check that column/key exists' in unicode(e): return raise except ProgrammingError, e: if engine == 'postgres' and u'does not exist' in unicode(e): return # In SQLAlchemy 0.4, the InternalError below is a ProgrammingError # and can't be executed in the upgrade transaction: if engine == 'postgres' and u'requires' in unicode(e): self.upgrade_session.bind.execute( 'ALTER TABLE "%s" DROP CONSTRAINT "%s"' % ( self.table.name, self._index_name(col))) return raise # Postgres constraints can be attached to tables and can't be dropped # at DB level. except InternalError, e: if engine == 'postgres': self.upgrade_session.execute( 'ALTER TABLE "%s" DROP CONSTRAINT "%s"' % ( self.table.name, self._index_name(col))) def drop_column(self, col_name): "Drop column col_name from table" session = self.upgrade_session log.debug(u"Dropping column %s from table %s", col_name, self.table.name) if session.bind.engine.name == 'sqlite': self._rebuild_sqlite({col_name: None}) else: session.execute('ALTER TABLE "%s" DROP COLUMN "%s";' % (self.table.name, col_name)) def rename_column(self, col, old_name): "Rename column from old_name to Column col" session = self.upgrade_session table = self._get_reflected_model() log.debug(u"Rename column %s to %s in table %s", old_name, col.name, table.name) if session.bind.engine.name == 'sqlite': self._rebuild_sqlite({old_name: col}) elif session.bind.engine.name == 'mysql': self.alter_column(col, old_name) else: session.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s";' % (table.name, old_name, col.name)) def alter_column(self, col, old_name=None, force_rebuild=False): """Change a column (possibly renaming from old_name) to Column col.""" session = self.upgrade_session table = self._get_reflected_model() log.debug(u"Altering column %s in table %s", col.name, table.name) sg = session.bind.dialect.schemagenerator(session.bind.dialect, session.bind) description = sg.get_column_specification(col) old_col = table.c[old_name or col.name] # SQLite doesn't enforce value length restrictions # only type changes have a real effect if session.bind.engine.name == 'sqlite': if not force_rebuild and ( (isinstance(col.type, (IbidUnicodeText, IbidUnicode)) and isinstance(old_col.type, (IbidUnicodeText, IbidUnicode) ) or (isinstance(col.type, Integer) and isinstance(old_col.type, Integer)))): return self._rebuild_sqlite( {old_name is None and col.name or old_name: col}) elif session.bind.engine.name == 'mysql': # Special handling for columns of TEXT type, because SQLAlchemy # can't create indexes for them recreate = [] if isinstance(col.type, IbidUnicodeText) \ or isinstance(old_col.type, IbidUnicodeText): for type in (table.constraints, table.indexes): for constraint in list(type): if any(True for column in constraint.columns if old_col.name == column.name): self.drop_index(constraint) constraint.columns = [ (old_col.name == column.name) and col or column for column in constraint.columns ] recreate.append(( isinstance(constraint, UniqueConstraint), self._mysql_constraint_createstring(constraint) )) session.execute('ALTER TABLE "%s" CHANGE "%s" %s;' % (table.name, old_col.name, description)) for unique, columnspec in recreate: session.execute('ALTER TABLE "%s" ADD %s INDEX (%s);' % (self.table.name, unique and 'UNIQUE' or '', columnspec)) else: if old_name is not None: self.rename_column(col, old_name) session.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s;' % (table.name, col.name, description.split(" ", 3)[1])) if old_col.nullable != col.nullable: session.execute( 'ALTER TABLE "%s" ALTER COLUMN "%s" %s NOT NULL;' % (table.name, col.name, col.nullable and 'DROP' or 'SET') ) def _rebuild_sqlite(self, colmap): """ SQLite doesn't support modification of table schema - must rebuild the table. colmap maps old column names to new Columns (or None for column deletion). Only modified columns need to be listed, unchaged columns are carried over automatically. Specify table in case name has changed in a more recent version. """ session = self.upgrade_session table = self._get_reflected_model() log.debug(u"Rebuilding SQLite table %s", table.name) fullcolmap = {} for col in table.c: if col.name in colmap: if colmap[col.name] is not None: fullcolmap[col.name] = colmap[col.name].name else: fullcolmap[col.name] = col.name for old, col in colmap.iteritems(): del table.c[old] if col is not None: table.append_column(col) session.execute('ALTER TABLE "%s" RENAME TO "%s_old";' % (table.name, table.name)) # SQLAlchemy indexes aren't attached to tables, they must be dropped # around now or we'll get a clash for constraint in table.indexes: try: constraint.drop() except OperationalError: pass table.create() session.execute('INSERT INTO "%s" ("%s") SELECT "%s" FROM "%s_old";' % ( table.name, '", "'.join(fullcolmap.values()), '", "'.join(fullcolmap.keys()), table.name )) session.execute('DROP TABLE "%s_old";' % table.name) # SQLAlchemy doesn't pick up all the indexes in the reflected table. # It's ok to use indexes that may be further in the future than this # upgrade because either we can already support them or we'll be # rebuilding again soon for constraint in self.table.indexes: try: constraint.create(bind=session.bind) except OperationalError: pass class SchemaVersionException(Exception): """There is an out-of-date table. The message should be a list of out of date tables. """ pass def schema_version_check(sessionmaker): """Pass through all tables, log out of date ones, and except if not all up to date""" session = sessionmaker() upgrades = [] for table in metadata.tables.itervalues(): if not hasattr(table, 'versioned_schema'): log.error("Table %s is not versioned.", table.name) continue if not table.versioned_schema.is_up_to_date(session): upgrades.append(table.name) if not upgrades: return raise SchemaVersionException(u", ".join(upgrades)) def upgrade_schemas(sessionmaker): "Pass through all tables and update schemas" # Make sure schema table is created first metadata.tables['schema'].versioned_schema.upgrade_schema(sessionmaker) for table in metadata.tables.itervalues(): if not hasattr(table, 'versioned_schema'): log.error("Table %s is not versioned.", table.name) continue table.versioned_schema.upgrade_schema(sessionmaker) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/db/__init__.py0000644000000000000000000000237611521776341014542 0ustar rootroot# Copyright (c) 2009-2011, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import warnings as _warnings from ibid.db.types import TypeDecorator, Integer, DateTime, Boolean, \ IbidUnicode, IbidUnicodeText from sqlalchemy import Table, Column, ForeignKey, Index, UniqueConstraint, \ PassiveDefault, or_, and_, MetaData as _MetaData from sqlalchemy.orm import eagerload, relation, synonym from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declarative_base as _declarative_base from sqlalchemy.exceptions import IntegrityError, SADeprecationWarning metadata = _MetaData() Base = _declarative_base(metadata=metadata) from ibid.db.versioned_schema import VersionedSchema, SchemaVersionException, \ schema_version_check, upgrade_schemas # We use SQLAlchemy 0.4 compatible .save_or_update() functions _warnings.filterwarnings('ignore', 'Use session.add\(\)', SADeprecationWarning) def get_regexp_op(session): "Return a regexp operator" if session.bind.engine.name in ('postgres', 'postgresql'): return lambda x, y: x.op('~')(y) else: return lambda x, y: x.op('REGEXP')(y) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/db/models.py0000644000000000000000000002276311362435477014275 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import datetime from ibid.db.types import IbidUnicode, IbidUnicodeText, Integer, DateTime from sqlalchemy import Table, Column, ForeignKey, UniqueConstraint from sqlalchemy.orm import relation from ibid.db import Base from ibid.db.versioned_schema import VersionedSchema class Schema(Base): __table__ = Table('schema', Base.metadata, Column('id', Integer, primary_key=True), Column('table', IbidUnicode(32), unique=True, nullable=False, index=True), Column('version', Integer, nullable=False), useexisting=True) class SchemaSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.table) def upgrade_2_to_3(self): self.drop_index(self.table.c.table) self.alter_column(Column('table', IbidUnicode(32), unique=True, nullable=False, index=True), force_rebuild=True) self.add_index(self.table.c.table) __table__.versioned_schema = SchemaSchema(__table__, 3) def __init__(self, table, version=0): self.table = table self.version = version def __repr__(self): return '' % self.table class Identity(Base): __table__ = Table('identities', Base.metadata, Column('id', Integer, primary_key=True), Column('account_id', Integer, ForeignKey('accounts.id'), index=True), Column('source', IbidUnicode(32, case_insensitive=True), nullable=False, index=True), Column('identity', IbidUnicodeText(32, case_insensitive=True), nullable=False, index=True), Column('created', DateTime), UniqueConstraint('source', 'identity'), useexisting=True) class IdentitySchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.account_id) self.add_index(self.table.c.source) self.add_index(self.table.c.identity) def upgrade_2_to_3(self): self.alter_column(Column('source', IbidUnicode(32), nullable=False, index=True)) self.alter_column(Column('identity', IbidUnicodeText, nullable=False, index=True)) def upgrade_3_to_4(self): self.drop_index(self.table.c.source) self.drop_index(self.table.c.identity) self.alter_column(Column('source', IbidUnicode(32, case_insensitive=True), nullable=False, index=True), force_rebuild=True) self.alter_column(Column('identity', IbidUnicodeText(32, case_insensitive=True), nullable=False, index=True), force_rebuild=True) self.add_index(self.table.c.source) self.add_index(self.table.c.identity) __table__.versioned_schema = IdentitySchema(__table__, 4) def __init__(self, source, identity, account_id=None): self.source = source self.identity = identity self.account_id = account_id self.created = datetime.utcnow() def __repr__(self): return '' % (self.identity, self.source) class Attribute(Base): __table__ = Table('account_attributes', Base.metadata, Column('id', Integer, primary_key=True), Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False, index=True), Column('name', IbidUnicode(32, case_insensitive=True), nullable=False, index=True), Column('value', IbidUnicodeText, nullable=False), UniqueConstraint('account_id', 'name'), useexisting=True) class AttributeSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.account_id) self.add_index(self.table.c.name) def upgrade_2_to_3(self): self.alter_column(Column('value', IbidUnicodeText, nullable=False)) def upgrade_3_to_4(self): self.drop_index(self.table.c.name) self.alter_column(Column('name', IbidUnicode(32, case_insensitive=True), nullable=False, index=True), force_rebuild=True) self.alter_column(Column('value', IbidUnicodeText, nullable=False), force_rebuild=True) self.add_index(self.table.c.name) __table__.versioned_schema = AttributeSchema(__table__, 4) def __init__(self, name, value): self.name = name self.value = value def __repr__(self): return '' % (self.name, self.value) class Credential(Base): __table__ = Table('credentials', Base.metadata, Column('id', Integer, primary_key=True), Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False, index=True), Column('source', IbidUnicode(32, case_insensitive=True), index=True), Column('method', IbidUnicode(16, case_insensitive=True), nullable=False, index=True), Column('credential', IbidUnicodeText, nullable=False), useexisting=True) class CredentialSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.account_id) self.add_index(self.table.c.source) self.add_index(self.table.c.method) def upgrade_2_to_3(self): self.alter_column(Column('source', IbidUnicode(32), index=True)) self.alter_column(Column('credential', IbidUnicodeText, nullable=False)) def upgrade_3_to_4(self): self.drop_index(self.table.c.source) self.drop_index(self.table.c.method) self.alter_column(Column('source', IbidUnicode(32, case_insensitive=True), index=True), force_rebuild=True) self.alter_column(Column('method', IbidUnicode(16, case_insensitive=True), nullable=False, index=True), force_rebuild=True) self.alter_column(Column('credential', IbidUnicodeText, nullable=False), force_rebuild=True) self.add_index(self.table.c.source) self.add_index(self.table.c.method) __table__.versioned_schema = CredentialSchema(__table__, 4) def __init__(self, method, credential, source=None, account_id=None): self.account_id = account_id self.source = source self.method = method self.credential = credential class Permission(Base): __table__ = Table('permissions', Base.metadata, Column('id', Integer, primary_key=True), Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False, index=True), Column('name', IbidUnicode(16, case_insensitive=True), nullable=False, index=True), Column('value', IbidUnicode(4, case_insensitive=True), nullable=False), UniqueConstraint('account_id', 'name'), useexisting=True) class PermissionSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.account_id) self.add_index(self.table.c.name) def upgrade_2_to_3(self): self.drop_index(self.table.c.name) self.alter_column(Column('name', IbidUnicode(16, case_insensitive=True), index=True), force_rebuild=True) self.alter_column(Column('value', IbidUnicode(4, case_insensitive=True), nullable=False, index=True), force_rebuild=True) self.add_index(self.table.c.name) __table__.versioned_schema = PermissionSchema(__table__, 3) def __init__(self, name=None, value=None): self.name = name self.value = value class Account(Base): __table__ = Table('accounts', Base.metadata, Column('id', Integer, primary_key=True), Column('username', IbidUnicode(32, case_insensitive=True), unique=True, nullable=False, index=True), useexisting=True) class AccountSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.username) def upgrade_2_to_3(self): self.drop_index(self.table.c.username) self.alter_column(Column('username', IbidUnicode(32, case_insensitive=True), unique=True, nullable=False, index=True), force_rebuild=True) self.add_index(self.table.c.username) __table__.versioned_schema = AccountSchema(__table__, 3) identities = relation(Identity, backref='account') attributes = relation(Attribute, cascade='all, delete-orphan') permissions = relation(Permission, cascade='all, delete-orphan') credentials = relation(Credential, cascade='all, delete-orphan') def __init__(self, username): self.username = username def __repr__(self): return '' % self.username # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/data/0000755000000000000000000000000011531277655012752 5ustar rootrootibid-0.1.1/ibid/data/tlds-alpha-by-domain.txt0000644000000000000000000000205311362435477017421 0ustar rootroot# Version 2009071300, Last Updated Mon Jul 13 07:07:02 2009 UTC AC AD AE AERO AF AG AI AL AM AN AO AQ AR ARPA AS ASIA AT AU AW AX AZ BA BB BD BE BF BG BH BI BIZ BJ BM BN BO BR BS BT BV BW BY BZ CA CAT CC CD CF CG CH CI CK CL CM CN CO COM COOP CR CU CV CX CY CZ DE DJ DK DM DO DZ EC EDU EE EG ER ES ET EU FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GOV GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN INFO INT IO IQ IR IS IT JE JM JO JOBS JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MG MH MIL MK ML MM MN MO MOBI MP MQ MR MS MT MU MUSEUM MV MW MX MY MZ NA NAME NC NE NET NF NG NI NL NO NP NR NU NZ OM ORG PA PE PF PG PH PK PL PM PN PR PRO PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR ST SU SV SY SZ TC TD TEL TF TG TH TJ TK TL TM TN TO TP TR TRAVEL TT TV TW TZ UA UG UK US UY UZ VA VC VE VG VI VN VU WF WS XN--0ZWM56D XN--11B5BS3A9AJ6G XN--80AKHBYKNJ4F XN--9T4B11YI5A XN--DEBA0AD XN--G6W251D XN--HGBK6AJ7F53BBA XN--HLCJ6AYA9ESC7A XN--JXALPDLP XN--KGBECHTV XN--ZCKZAH YE YT YU ZA ZM ZW ibid-0.1.1/ibid/data/README0000644000000000000000000000026211362435477013632 0ustar rootrootThese are data files used by plugins, that don't change very much. Thus outdated versions shouldn't be a major issue. Sources: http://data.iana.org/TLD/tlds-alpha-by-domain.txt ibid-0.1.1/ibid/source/0000755000000000000000000000000011531277655013341 5ustar rootrootibid-0.1.1/ibid/source/pb.py0000644000000000000000000000307111362435477014315 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from twisted.spread import pb from twisted.application import internet from twisted.internet import reactor import ibid from ibid.source import IbidSourceFactory from ibid.config import IntOption from ibid.event import Event class IbidRoot(pb.Root): def __init__(self, name): self.name = name self.log = logging.getLogger('sources.%s' % name) def respond(self, event): return [response['reply'] for response in event.responses] def remote_message(self, message): event = Event(self.name, u'message') event.sender['connection'] = event.sender['id'] = event.sender['nick'] = event.channel = self.name event.addressed = True event.public = False event.message = unicode(message, 'utf-8', 'replace') self.log.debug(u'message("%s")' % event.message) return ibid.dispatcher.dispatch(event).addCallback(self.respond) def remote_get_plugin(self, plugin): self.log.debug(u'get_plugin("%s")' % plugin) return ibid.rpc[plugin] class SourceFactory(IbidSourceFactory): supports = ('multiline',) port = IntOption('port', 'Port number to listen on', 8789) def setServiceParent(self, service): root = pb.PBServerFactory(IbidRoot(self.name)) if service: return internet.TCPServer(self.port, root).setServiceParent(service) else: reactor.listenTCP(self.port, root) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/timer.py0000644000000000000000000000145611362435477015041 0ustar rootroot# Copyright (c) 2008-2009, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from twisted.application import internet import ibid from ibid.config import IntOption from ibid.event import Event from ibid.source import IbidSourceFactory class SourceFactory(IbidSourceFactory): step = IntOption('step', 'Timer interval in seconds', 1) def tick(self): event = Event(self.name, u'clock') ibid.dispatcher.dispatch(event) def setServiceParent(self, service): self.s = internet.TimerService(self.step, self.tick) if service is None: self.s.startService() else: self.s.setServiceParent(service) def disconnect(self): self.s.stopService() return True # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/telnet.py0000644000000000000000000000525411362435477015214 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from twisted.internet import protocol, reactor from twisted.conch import telnet from twisted.application import internet import ibid from ibid.source import IbidSourceFactory from ibid.config import IntOption from ibid.event import Event class TelnetProtocol(telnet.StatefulTelnetProtocol): state = 'User' def connectionMade(self): self.factory.send = self.send self.transport.write('Username: ') def telnet_User(self, line): self.user = unicode(line.strip(), 'utf-8', 'replace') if ' ' in self.user: self.transport.write('Sorry, no spaces allowed in usernames\r\n') self.factory.log.info(u"Rejected connection from %s", self.user) self.transport.loseConnection() return self.factory.log.info(u"Connection established with %s", self.user) return 'Query' def telnet_Query(self, line): event = Event(self.factory.name, u'message') event.message = unicode(line.strip(), 'utf-8', 'replace') event.sender['connection'] = self.user event.sender['id'] = self.user event.sender['nick'] = event.sender['connection'] event.channel = event.sender['connection'] event.addressed = True event.public = False self.factory.log.debug(u"Received message from %s: %s", self.user, event.message) ibid.dispatcher.dispatch(event).addCallback(self.respond) return 'Query' def respond(self, event): for response in event.responses: self.send(response) def send(self, response): self.transport.write(response['reply'].encode('utf-8') + '\n') self.factory.log.debug(u"Sent message to %s: %s", self.user, response['reply']) class SourceFactory(protocol.ServerFactory, IbidSourceFactory): protocol = TelnetProtocol supports = ('multiline',) port = IntOption('port', 'Port to listen on', 3000) def __init__(self, name, *args, **kwargs): #protocol.ServerFactory.__init__(self, *args, **kwargs) IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) def setServiceParent(self, service=None): if service: self.listener = internet.TCPServer(self.port, self).setServiceParent(service) return self.listener else: self.listener = reactor.listenTCP(self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.listener.stopListening() return True # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/dc.py0000644000000000000000000002030211362435477014276 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from time import sleep import logging import dcwords from twisted.internet import reactor from twisted.internet import protocol from twisted.application import internet import ibid from ibid.config import Option, IntOption, FloatOption from ibid.source import IbidSourceFactory from ibid.event import Event from ibid.utils import ibid_version class DCBot(dcwords.DCClient): version = ibid_version() or 'dev' client = 'Ibid' def connectionMade(self): self.keepalive = True self.ping_interval = self.factory.ping_interval self.pong_timeout = self.factory.pong_timeout self.my_nickname = self.factory.nick self.my_password = self.factory.password self.my_interest = self.factory.interest self.my_speed = self.factory.speed self.my_email = self.factory.email self.my_sharesize = self.factory.sharesize self.my_slots = self.factory.slots dcwords.DCClient.connectionMade(self) self.factory.resetDelay() self.factory.send = self.send self.factory.proto = self self.auth_callbacks = {} self.factory.log.info(u"Connected") def connectionLost(self, reason): self.factory.log.info(u"Disconnected (%s)", reason) event = Event(self.factory.name, u'source') event.status = u'disconnected' ibid.dispatcher.dispatch(event) dcwords.DCClient.connectionLost(self, reason) def signedOn(self): names = ibid.config.plugins['core']['names'] if self.my_nickname not in names: self.factory.log.info(u'Adding "%s" to plugins.core.names', self.my_nickname) names.append(self.my_nickname) ibid.config.plugins['core']['names'] = names ibid.reloader.reload_config() event = Event(self.factory.name, u'source') event.status = u'connected' ibid.dispatcher.dispatch(event) event = Event(self.factory.name, u'source') event.channel = u'$public' event.status = u'joined' ibid.dispatcher.dispatch(event) self.factory.log.info(u"Signed on") def _create_event(self, type, user): event = Event(self.factory.name, type) event.sender['connection'] = user event.sender['id'] = user event.sender['nick'] = user event.channel = u'$public' event.public = True return event def _state_event(self, user, action): event = self._create_event(u'state', user) event.state = action ibid.dispatcher.dispatch(event).addCallback(self.respond) def _message_event(self, msgtype, user, private, msg): event = self._create_event(msgtype, user) event.message = msg self.factory.log.debug(u'Received %s from %s in %s: %s', msgtype, event.sender['id'], private and u'private' or u'public', event.message) if private: event.addressed = True event.public = False event.channel = event.sender['connection'] else: event.public = True ibid.dispatcher.dispatch(event).addCallback(self.respond) def privmsg(self, user, private, msg): self._message_event(u'message', user, private, msg) def userJoined(self, user): self._state_event(user, u'online') def userQuit(self, user): self._state_event(user, u'offline') def respond(self, event): for response in event.responses: self.send(response) def send(self, response): message = response['reply'] if message: for prefix in self.factory.banned_prefixes: if message.startswith(prefix): self.factory.log.info(u'Suppressed banned response: %s', message) return target = response['target'] if target == '$public': target = None if response.get('topic', False): self.topic(message) self.factory.log.debug(u'Set topic to %s', message) elif response.get('action', False): if self.factory.action_prefix and target is None: self.say(target, u'%s %s' % (self.factory.action_prefix, message)) elif self.factory.action_prefix: self.say(target, u'*%s*' % message) else: self.say(target, message) self.factory.log.debug(u"Sent action to %s: %s", target, message) else: self.say(target, message) self.factory.log.debug(u"Sent privmsg to %s: %s", target, message) def authenticate(self, nick, callback): self.auth_callbacks[nick] = callback self.sendLine('$GetNickList') def dc_OpList(self, params): dcwords.DCClient.dc_OpList(self, params) done = [] for nick, callback in self.auth_callbacks.iteritems(): if nick in self.hub_users and self.hub_users[nick].op is True: callback(nick, True) else: callback(nick, False) done.append(nick) for nick in done: del self.auth_callbacks[nick] class SourceFactory(protocol.ReconnectingClientFactory, IbidSourceFactory): protocol = DCBot supports = ['multiline', 'topic'] auth = ('op',) port = IntOption('port', 'Server port number', 411) server = Option('server', 'Server hostname') nick = Option('nick', 'DC nick', ibid.config['botname']) password = Option('password', 'Password', None) interest = Option('interest', 'User Description', '') speed = Option('speed', 'Bandwidth', '1kbps') email = Option('email', 'eMail Address', 'http://ibid.omnia.za.net/') sharesize = IntOption('sharesize', 'DC Share Size (bytes)', 0) slots = IntOption('slots', 'DC Open Slots', 0) action_prefix = Option('action_prefix', 'Command for actions (i.e. +me)', None) banned_prefixes = Option('banned_prefixes', 'Prefixes not allowed in bot responses, i.e. !', '') max_message_length = IntOption('max_message_length', 'Maximum length of messages', 490) ping_interval = FloatOption('ping_interval', 'Seconds idle before sending a PING', 60) pong_timeout = FloatOption('pong_timeout', 'Seconds to wait for PONG', 300) # ReconnectingClient uses this: maxDelay = IntOption('max_delay', 'Max seconds to wait inbetween reconnects', 900) factor = FloatOption('delay_factor', 'Factor to multiply delay inbetween reconnects by', 2) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % self.name) self._auth = {} def setup(self): if self.action_prefix is None and 'action' in self.supports: self.supports.remove('action') if self.action_prefix is not None and 'action' not in self.supports: self.supports.append('action') def setServiceParent(self, service): if service: internet.TCPClient(self.server, self.port, self).setServiceParent(service) else: reactor.connectTCP(self.server, self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() if hasattr(self, 'proto'): self.proto.transport.loseConnection() return True def truncation_point(self, response, event=None): return self.max_message_length def _dc_auth_callback(self, nick, result): self._auth[nick] = result def auth_op(self, event, credential): nick = event.sender['nick'] if nick in self.proto.hub_users and self.proto.hub_users[nick].op in (True, False): return self.proto.hub_users[nick].op reactor.callFromThread(self.proto.authenticate, nick, self._dc_auth_callback) for i in xrange(150): if nick in self._auth: break sleep(0.1) if nick in self._auth: result = self._auth[nick] del self._auth[nick] return result def url(self): return u'dc://%s@%s:%s' % (self.nick, self.server, self.port) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/__init__.py0000644000000000000000000000363711362435477015463 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from copy import copy class IbidSourceFactory(object): supports = () auth = () permissions = () def __new__(cls, *args): cls.type = cls.__module__.split('.')[2] for name, option in options.items(): new = copy(option) default = getattr(cls, name) new.default = default setattr(cls, name, new) return super(IbidSourceFactory, cls).__new__(cls, *args) def __init__(self, name): self.name = name self.setup() def setup(self): "Apply configuration. Called on every config reload" pass def setServiceParent(self, service): "Start the source and connect" raise NotImplementedError def connect(self): "Connect (if disconncted)" return self.setServiceParent(None) def disconnect(self): "Disconnect source" raise NotImplementedError def url(self): "Return a URL describing the source" return None def logging_name(self, identity): "Given an identity or connection, return a name suitable for logging" return identity def truncation_point(self, response, event=None): """Given a target, and possibly a related event, return the number of bytes to clip at, or None to indicate that a complete message will be delivered. """ if (event is not None and response.get('target', None) == event.get('channel', None) and event.get('public', True)): return 490 return None from ibid.config import Option options = { 'auth': Option('auth', 'Authentication methods to allow'), 'permissions': Option('permissions', 'Permissions granted to users on this source') } # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/irc.py0000644000000000000000000003707711505416204014470 0ustar rootroot# Copyright (c) 2008-2010, Jonathan Hitchcock, Michael Gorven, Stefano Rivera, # Max Rabkin # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from fnmatch import fnmatch from time import sleep import logging from threading import Lock from twisted.internet import reactor from twisted.words.protocols import irc from twisted.internet import protocol, ssl from twisted.application import internet from sqlalchemy import or_ import ibid from ibid.config import Option, IntOption, BoolOption, FloatOption, ListOption from ibid.db.models import Credential from ibid.source import IbidSourceFactory from ibid.event import Event from ibid.utils import ibid_version class Ircbot(irc.IRCClient): _ping_deferred = None _reconnect_deferred = None def connectionMade(self): self.nickname = self.factory.nick.encode('utf-8') self.realname = self.factory.realname.encode('utf-8') irc.IRCClient.connectionMade(self) self.factory.resetDelay() self.factory.proto = self self.auth_callbacks = {} self.mode_prefixes = '@+' self._ping_deferred = reactor.callLater(self.factory.ping_interval, self._idle_ping) self.factory.log.info(u"Connected") def connectionLost(self, reason): self.factory.log.info(u"Disconnected (%s)", reason) event = Event(self.factory.name, u'source') event.status = u'disconnected' ibid.dispatcher.dispatch(event) irc.IRCClient.connectionLost(self, reason) def _idle_ping(self): self.factory.log.log(logging.DEBUG - 5, u'Sending idle PING') self._ping_deferred = None self._reconnect_deferred = reactor.callLater(self.factory.pong_timeout, self._timeout_reconnect) self.sendLine('PING idle-ibid') def _timeout_reconnect(self): self.factory.log.info(u'Ping-Pong timeout. Reconnecting') self.transport.loseConnection() def irc_PONG(self, prefix, params): if params[-1] == 'idle-ibid' and self._reconnect_deferred is not None: self.factory.log.log(logging.DEBUG - 5, u'Received PONG') self._reconnect_deferred.cancel() self._reconnect_deferred = None self._ping_deferred = reactor.callLater(self.factory.ping_interval, self._idle_ping) def dataReceived(self, data): irc.IRCClient.dataReceived(self, data) if self._ping_deferred is not None: self._ping_deferred.reset(self.factory.ping_interval) def sendLine(self, line): irc.IRCClient.sendLine(self, line) if self._ping_deferred is not None: self._ping_deferred.reset(self.factory.ping_interval) def signedOn(self): names = ibid.config.plugins['core']['names'] if self.nickname not in names: self.factory.log.info(u'Adding "%s" to plugins.core.names', self.nickname) names.append(self.nickname) ibid.config.plugins['core']['names'] = names ibid.reloader.reload_config() if self.factory.modes: self.mode(self.nickname, True, self.factory.modes.encode('utf-8')) self.ctcpMakeQuery(self.nickname, [('HOSTMASK', None)]) for channel in self.factory.channels: self.join(channel.encode('utf-8')) self.factory.log.info(u"Signed on") event = Event(self.factory.name, u'source') event.status = u'connected' ibid.dispatcher.dispatch(event) def _create_event(self, type, user, channel): nick = user.split('!', 1)[0] event = Event(self.factory.name, type) event.sender['connection'] = user event.sender['id'] = nick event.sender['nick'] = event.sender['id'] event.channel = channel event.public = True return event def _state_event(self, user, channel, action, kicker=None, message=None, othername=None): event = self._create_event(u'state', user, channel) event.state = action if message: event.message = message if kicker: event.kicker = kicker self.factory.log.debug(u"%s has been kicked from %s by %s (%s)", event.sender['id'], event.channel, event.kicker, event.message) elif othername: event.othername = othername ibid.dispatcher.dispatch(event).addCallback(self.respond) def privmsg(self, user, channel, msg): self._message_event(u'message', user, channel, msg) def noticed(self, user, channel, msg): self._message_event(u'notice', user, channel, msg) def action(self, user, channel, msg): self._message_event(u'action', user, channel, msg) def _message_event(self, msgtype, user, channel, msg): user = unicode(user, 'utf-8', 'replace') channel = unicode(channel, 'utf-8', 'replace') event = self._create_event(msgtype, user, channel) event.message = unicode(msg, 'utf-8', 'replace') self.factory.log.debug(u"Received %s from %s in %s: %s", msgtype, event.sender['id'], event.channel, event.message) if channel.lower() == self.nickname.lower(): event.addressed = True event.public = False event.channel = event.sender['connection'] else: event.public = True ibid.dispatcher.dispatch(event).addCallback(self.respond) def userJoined(self, user, channel): user = unicode(user, 'utf-8', 'replace') channel = unicode(channel, 'utf-8', 'replace') self._state_event(user, channel, u'online') def userLeft(self, user, channel): user = unicode(user, 'utf-8', 'replace') channel = unicode(channel, 'utf-8', 'replace') self._state_event(user, channel, u'offline') def userRenamed(self, oldname, newname): oldname = unicode(oldname, 'utf-8', 'replace') newname = unicode(newname, 'utf-8', 'replace') self._state_event(oldname, None, u'offline', othername=newname) self._state_event(newname, None, u'online', othername=oldname) def userQuit(self, user, channel): # Channel contains the quit message user = unicode(user, 'utf-8', 'replace') channel = unicode(channel, 'utf-8', 'replace') self._state_event(user, None, u'offline', message=channel) def userKicked(self, kickee, channel, kicker, message): kickee = unicode(kickee, 'utf-8', 'replace') channel = unicode(channel, 'utf-8', 'replace') kicker = unicode(kicker, 'utf-8', 'replace') message = unicode(message, 'utf-8', 'replace') self._state_event(kickee, channel, u'kicked', kicker, message) def respond(self, event): for response in event.responses: self.send(response) def send(self, response): message = response['reply'] raw_message = message.encode('utf-8') # Target may be a connection or a plain nick target = response['target'].split('!')[0] raw_target = target.encode('utf-8') if response.get('topic', False): self.topic(raw_target, raw_message) self.factory.log.debug(u"Set topic in %s to %s", target, message) elif response.get('action', False): # We can't use self.me() because it prepends a # onto channel names # See http://twistedmatrix.com/trac/ticket/3910 self.ctcpMakeQuery(raw_target, [('ACTION', raw_message)]) self.factory.log.debug(u"Sent action to %s: %s", target, message) elif response.get('notice', False): self.notice(raw_target, raw_message) self.factory.log.debug(u"Sent notice to %s: %s", target, message) else: self.msg(raw_target, raw_message) self.factory.log.debug(u"Sent privmsg to %s: %s", target, message) def join(self, channel): self.factory.log.info(u"Joining %s", channel) irc.IRCClient.join(self, channel.encode('utf-8')) def joined(self, channel): event = Event(self.factory.name, u'source') event.channel = channel event.status = u'joined' ibid.dispatcher.dispatch(event) def leave(self, channel): self.factory.log.info(u"Leaving %s", channel) irc.IRCClient.leave(self, channel.encode('utf-8')) def left(self, channel): event = Event(self.factory.name, u'source') event.channel = channel event.status = u'left' ibid.dispatcher.dispatch(event) def authenticate(self, nick, callback): if nick in self.auth_callbacks: self.auth_callbacks[nick].append(callback) else: self.auth_callbacks[nick] = [callback] self.sendLine('WHOIS %s' % nick.encode('utf-8')) def do_auth_callback(self, nick, result): if nick in self.auth_callbacks: self.factory.log.debug(u"Authentication result for %s: %s", nick, result) for callback in self.auth_callbacks[nick]: callback(result) del self.auth_callbacks[nick] def irc_unknown(self, prefix, command, params): if command == '307' and len(params) == 3 and params[2] == 'is a registered nick': self.do_auth_callback(params[1], True) elif command == '307' and len(params) == 3 and params[2] == 'user has identified to services': self.do_auth_callback(params[1], True) elif command == '307' and len(params) == 3 and params[2] == 'has identified for this nick': self.do_auth_callback(params[1], True) elif command == '320' and len(params) == 3 and params[2] == 'is identified to services ': self.do_auth_callback(params[1], True) elif command == '330' and len(params) == 4 and params[3] == 'is logged in as': self.do_auth_callback(params[1], True) elif command == "RPL_ENDOFWHOIS": self.do_auth_callback(params[1], False) def irc_RPL_BOUNCE(self, prefix, params): # Broken in IrcClient :/ # See http://twistedmatrix.com/trac/ticket/3285 if params[-1] in ('are available on this server', 'are supported by this server'): self.isupport(params[1:-1]) else: self.bounce(params[1]) def isupport(self, options): "Server supports message" for option in options: if option.startswith('PREFIX='): self.mode_prefixes = option.split(')', 1)[1] def irc_RPL_NAMREPLY(self, prefix, params): channel = params[2] for user in params[3].split(): if user[0] in self.mode_prefixes: user = user[1:] if user != self.nickname: self.userJoined(user, channel) def ctcpQuery_VERSION(self, user, channel, data): nick = user.split("!")[0] self.ctcpMakeReply(nick, [('VERSION', 'Ibid %s' % (ibid_version() or '',))]) def ctcpQuery_SOURCE(self, user, channel, data): nick = user.split("!")[0] self.ctcpMakeReply(nick, [('SOURCE', 'http://ibid.omnia.za.net/')]) def ctcpUnknownQuery(self, user, channel, tag, data): if user.split('!')[0] == self.nickname and tag == 'HOSTMASK': self.hostmask = user self.factory.log.debug(u"Set hostmask to %s", self.hostmask) else: irc.IRCClient.ctcpUnknownQuery(self, user, channel, tag, data) class SourceFactory(protocol.ReconnectingClientFactory, IbidSourceFactory): protocol = Ircbot auth = ('hostmask', 'nickserv') supports = ('action', 'notice', 'topic') port = IntOption('port', 'Server port number', 6667) ssl = BoolOption('ssl', 'Use SSL', False) server = Option('server', 'Server hostname') nick = Option('nick', 'IRC nick', ibid.config['botname']) realname = Option('realname', 'Full Name', ibid.config['botname']) modes = Option('modes', 'User modes to set') channels = ListOption('channels', 'Channels to autojoin', []) ping_interval = FloatOption('ping_interval', 'Seconds idle before sending a PING', 60) pong_timeout = FloatOption('pong_timeout', 'Seconds to wait for PONG', 300) # ReconnectingClient uses this: maxDelay = IntOption('max_delay', 'Max seconds to wait inbetween reconnects', 900) factor = FloatOption('delay_factor', 'Factor to multiply delay inbetween reconnects by', 2) def __init__(self, name): IbidSourceFactory.__init__(self, name) self._auth = {} self._auth_ticket = 0 self._auth_ticket_lock = Lock() self.log = logging.getLogger('source.%s' % self.name) def setServiceParent(self, service): if self.ssl: sslctx = ssl.ClientContextFactory() if service: internet.SSLClient(self.server, self.port, self, sslctx).setServiceParent(service) else: reactor.connectSSL(self.server, self.port, self, sslctx) else: if service: internet.TCPClient(self.server, self.port, self).setServiceParent(service) else: reactor.connectTCP(self.server, self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() if hasattr(self, 'proto'): self.proto.transport.loseConnection() return True def join(self, channel): return self.proto.join(channel) def leave(self, channel): return self.proto.leave(channel) def change_nick(self, nick): return self.proto.setNick(nick.encode('utf-8')) def send(self, response): return self.proto.send(response) def logging_name(self, identity): if identity is None: return u'' return identity.split(u'!')[0] def truncation_point(self, response, event=None): target = response['target'].split('!')[0] raw_target = target.encode('utf-8') if hasattr(self.proto, 'hostmask'): hostmask_len = len(self.proto.hostmask) else: hostmask_len = 50 # max = 512 - len(':' + hostmask + ' ' + command + ' ' + target + ' :\r\n') cmds = {'notice': len('NOTICE'), 'topic': len('TOPIC'), 'action': len('PRIVMSG\001ACTION \001')} for cmd, command_len in cmds.items(): if response.get(cmd, False): break else: command_len = len('PRIVMSG') return 505 - command_len - len(raw_target) - hostmask_len def url(self): return u'irc://%s@%s:%s' % (self.nick, self.server, self.port) def auth_hostmask(self, event, credential = None): for credential in event.session.query(Credential) \ .filter_by(method=u'hostmask', account_id=event.account) \ .filter(or_(Credential.source == event.source, Credential.source == None)) \ .all(): if fnmatch(event.sender['connection'], credential.credential): return True def auth_nickserv(self, event, credential): self._auth_ticket_lock.acquire() self._auth_ticket += 1 ticket = self._auth_ticket self._auth_ticket_lock.release() def callback(result): self._auth[ticket] = result reactor.callFromThread(self.proto.authenticate, event.sender['nick'], callback) # We block in the plugin thread for up to this long, waiting for # NickServ to reply wait = 15 for i in xrange(wait * 10): if ticket in self._auth: break sleep(0.1) if ticket in self._auth: result = self._auth[ticket] del self._auth[ticket] return result # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/smtp.py0000644000000000000000000001307311362435477014702 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from datetime import datetime from email import message_from_string from socket import gethostname import re from twisted.application import internet from twisted.internet import defer, reactor from twisted.mail import smtp from zope.interface import implements import ibid from ibid.compat import email_utils from ibid.config import Option, IntOption, ListOption from ibid.event import Event from ibid.source import IbidSourceFactory stripsig = re.compile(r'^-- $.*', re.M+re.S) class IbidDelivery: implements(smtp.IMessageDelivery) def __init__(self, factory): self.factory = factory def receivedHeader(self, helo, origin, recipients): return 'Received: from %s ([%s])\n\tby %s (Ibid)\n\tfor %s; %s' % ( helo[0], helo[1], gethostname(), str(recipients[0]), datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +0000 (UTC)') ) def validateFrom(self, helo, origin): return origin def validateTo(self, user): if str(user) == self.factory.address or str(user) in self.factory.accept: return lambda: Message(self.factory.name) raise smtp.SMTPBadRcpt(user) class Message: implements(smtp.IMessage) def __init__(self, name): self.lines = [] self.name = name self.log = logging.getLogger('source.%s' % name) def lineReceived(self, line): self.lines.append(line) def eomReceived(self): mail = message_from_string('\n'.join(self.lines)) event = Event(self.name, u'message') (realname, address) = email_utils.parseaddr(mail['from']) event.channel = event.sender['connection'] = event.sender['id'] = unicode(address, 'utf-8', 'replace') event.sender['nick'] = realname != '' and unicode(realname, 'utf-8', 'replace') or event.channel event.public = False event.addressed = True event.subject = unicode(mail['subject'], 'utf-8', 'replace') event.headers = dict((i[0].lower(), unicode(i[1], 'utf-8', 'replace')) for i in mail.items()) message = mail.is_multipart() and mail.get_payload()[0].get_payload() or mail.get_payload() if len(message) > 0: event.message = stripsig.sub('', unicode(message, 'utf-8', 'replace')).strip().replace('\n', ' ') else: event.message = event.subject self.log.debug(u"Received message from %s: %s", event.sender['connection'], event.message) ibid.dispatcher.dispatch(event).addCallback(ibid.sources[self.name.lower()].respond) return defer.succeed(None) def connectionLost(self): self.lines = None class SourceFactory(IbidSourceFactory, smtp.SMTPFactory): supports = ('multiline',) port = IntOption('port', 'Port number to listen on', 10025) address = Option('address', 'Email address to accept messages for and send from', 'ibid@localhost') accept = ListOption('accept', 'Email addresses to accept messages for', []) relayhost = Option('relayhost', 'SMTP server to relay outgoing messages to') def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) self.delivery = IbidDelivery(self) def buildProtocol(self, addr): p = smtp.SMTPFactory.buildProtocol(self, addr) p.delivery = self.delivery return p def setServiceParent(self, service): self.service = service if service: internet.TCPServer(self.port, self).setServiceParent(service) else: reactor.listenTCP(self.port, self) def url(self): return u'mailto:%s' % (self.address,) def respond(self, event): messages = {} for response in event.responses: if response['target'] not in messages: messages[response['target']] = response else: messages[response['target']]['reply'] += '\n' + response['reply'] for message in messages.values(): if 'subject' not in message: message['Subject'] = 'Re: ' + event['subject'] if 'message-id' in event.headers: response['In-Reply-To'] = event.headers['message-id'] if 'references' in event.headers: response['References'] = '%(references)s %(message-id)s' % event.headers elif 'in-reply-to' in event.headers: response['References'] = '%(in-reply-to)s %(message-id)s' % event.headers else: response['References'] = '%(message-id)s' % event.headers self.send(message) def send(self, response): message = response['reply'] response['To'] = response['target'] response['Date'] = smtp.rfc822date() if 'Subject' not in response: response['Subject'] = 'Message from %s' % ibid.config['botname'] response['Content-Type'] = 'text/plain; charset=utf-8' del response['target'] del response['source'] del response['reply'] body = '' for header, value in response.items(): body += '%s: %s\n' % (header, value) body += '\n' body += message port = ':' in self.relayhost and int(self.relayhost.split(':')[1]) or 25 smtp.sendmail(self.relayhost.split(':')[0], self.address, response['To'], body.encode('utf-8'), port=port) self.log.debug(u"Sent email to %s: %s", response['To'], response['Subject']) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/http.py0000644000000000000000000001013211362435477014667 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from twisted.web import server, resource, static, xmlrpc, soap from twisted.application import internet from twisted.internet import reactor from jinja import Environment, FileSystemLoader import ibid from ibid.source import IbidSourceFactory from ibid.event import Event from ibid.config import Option, IntOption from ibid.utils import locate_resource templates = Environment(loader=FileSystemLoader( locate_resource('ibid', 'templates'))) class Index(resource.Resource): def __init__(self, name, *args, **kwargs): resource.Resource.__init__(self, *args, **kwargs) self.name = name self.log = logging.getLogger('source.%s' % name) self.template = templates.get_template('index.html') def render_GET(self, request): return self.template.render(rpc=ibid.rpc.keys()).encode('utf-8') class Message(resource.Resource): def __init__(self, name, *args, **kwargs): resource.Resource.__init__(self, *args, **kwargs) self.name = name self.log = logging.getLogger('source.%s' % name) self.form_template = templates.get_template('message_form.html') def render_GET(self, request): if 'm' in request.args: return self.render_POST(request) return self.form_template.render().encode('utf-8') def render_POST(self, request): event = Event(self.name, u'message') event.sender['nick'] = event.sender['id'] = event.sender['connection'] = event.channel = unicode(request.transport.getPeer().host) event.addressed = True event.public = False event.message = unicode(request.args['m'][0], 'utf-8', 'replace') self.log.debug(u"Received GET request from %s: %s", event.sender['connection'], event.message) ibid.dispatcher.dispatch(event).addCallback(self.respond, request) return server.NOT_DONE_YET def respond(self, event, request): output = '\n'.join([response['reply'].encode('utf-8') for response in event.responses]) request.setHeader('Content-Type', 'text/plain; charset=utf-8') request.write(output) request.finish() self.log.debug(u"Responded to request from %s: %s", event.sender['connection'], output) class Plugin(resource.Resource): def __init__(self, name, *args, **kwargs): resource.Resource.__init__(self, *args, **kwargs) self.name = name def getChild(self, path, request): return path in ibid.rpc and ibid.rpc[path] or None class XMLRPC(xmlrpc.XMLRPC): def _getFunction(self, functionPath): if functionPath.find(self.separator) != -1: plugin, functionPath = functionPath.split(self.separator, 1) object = ibid.rpc[plugin] else: object = self return getattr(object, 'remote_%s' % functionPath) class SOAP(soap.SOAPPublisher): separator = '.' def lookupFunction(self, functionName): if functionName.find(self.separator) != -1: plugin, functionName = functionName.split(self.separator, 1) object = ibid.rpc[plugin] else: object = self return getattr(object, 'remote_%s' % functionName) class SourceFactory(IbidSourceFactory): port = IntOption('port', 'Port number to listen on', 8080) myurl = Option('url', 'URL to advertise') def __init__(self, name): IbidSourceFactory.__init__(self, name) root = Plugin(name) root.putChild('', Index(name)) root.putChild('message', Message(name)) root.putChild('static', static.File(locate_resource('ibid', 'static'))) root.putChild('RPC2', XMLRPC()) root.putChild('SOAP', SOAP()) self.site = server.Site(root) def setServiceParent(self, service): if service: return internet.TCPServer(self.port, self.site).setServiceParent(service) else: reactor.listenTCP(self.port, self.site) def url(self): return self.myurl # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/campfire.py0000644000000000000000000001116111362435477015501 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from campfirewords import CampfireClient import ibid from ibid.config import BoolOption, IntOption, Option, ListOption from ibid.event import Event from ibid.source import IbidSourceFactory class CampfireBot(CampfireClient): def __init__(self, factory): self.factory = factory CampfireClient.__init__(self, self.factory.subdomain, self.factory.token, self.factory.rooms, self.factory.secure, self.factory.keepalive_timeout) def _create_event(self, type, user_id, user_name, room_id, room_name): event = Event(self.factory.name, type) if user_id is not None: # user_id is an int. simplejson may have returned a str() if # user_name and room_name don't contain non-ASCII chars. user_id = unicode(user_id) user_name = unicode(user_name) event.sender['connection'] = user_id event.sender['id'] = user_id event.sender['nick'] = user_name event.channel = unicode(room_name) event.public = True event.source = self.factory.name return event def _message_event(self, body, type=u'message', **kwargs): event = self._create_event(type, **kwargs) event.message = unicode(body) self.factory.log.debug(u'Received %s from %s in %s: %s', type, kwargs['user_name'], kwargs['room_name'], body) ibid.dispatcher.dispatch(event).addCallback(self.respond) def _state_event(self, state, **kwargs): event = self._create_event(u'state', **kwargs) event.state = state self.factory.log.debug(u'%s in %s is now %s', kwargs['user_name'], kwargs['room_name'], state) ibid.dispatcher.dispatch(event).addCallback(self.respond) def handle_Text(self, **kwargs): self._message_event(**kwargs) def handle_TopicChange(self, **kwargs): self._message_event(type=u'topic', **kwargs) def handle_Leave(self, **kwargs): self._state_event(state=u'offline', **kwargs) def handle_Enter(self, **kwargs): self._state_event(state=u'online', **kwargs) def joined_room(self, room_info): self._message_event(type=u'topic', body=room_info['topic'], user_id=None, user_name=None, room_id=room_info['id'], room_name=room_info['name']) def send(self, response): message = response['reply'] if response.get('action', False): message = u'*%s*' % message elif response.get('topic', False): self.topic(response['target'], message) return if '\n' in message: self.say(response['target'], message, type='PasteMessage') else: self.say(response['target'], message) def respond(self, event): for response in event.responses: self.send(response) def join(self, room_name): return self.join_room(self._locate_room(room_name)) def leave(self, room_name): return self.leave_room(self._locate_room(room_name)) class SourceFactory(IbidSourceFactory): auth = ('implicit',) supports = ('action', 'multiline', 'topic') subdomain = Option('subdomain', 'Campfire subdomain') secure = BoolOption('secure', 'Use https (paid accounts only)', False) token = Option('token', 'Campfire token') rooms = ListOption('rooms', 'Rooms to join', []) keepalive_timeout = IntOption('keepalive_timeout', 'Stream keepalive timeout. ' 'Campfire sends a keepalive every <5 seconds', 30) def __init__(self, name): super(SourceFactory, self).__init__(name) self.log = logging.getLogger('source.%s' % self.name) self.client = CampfireBot(self) def setServiceParent(self, service): self.client.connect() def disconnect(self): self.client.disconnect() return True def url(self): protocol = self.secure and 'https' or 'http' return '%s://%s.campfirenow.com/' % (protocol, self.subdomain) def send(self, response): return self.client.send(response) def join(self, room_name): return self.client.join(room_name) def leave(self, room_name): return self.client.leave(room_name) def truncation_point(self, response, event=None): return None # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/silc.py0000644000000000000000000002265211505416204014636 0ustar rootroot# Copyright (c) 2009-2010, Jonathan Hitchcock, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. # This source requires Python >= 2.5: from __future__ import absolute_import from os.path import join, exists from twisted.application import internet from silc import SilcClient, create_key_pair, load_key_pair import ibid from ibid.event import Event from ibid.source import IbidSourceFactory from ibid.config import Option, IntOption, ListOption import logging class SilcBot(SilcClient): def __init__(self, keys, nick, ident, name, factory): self.nick = nick SilcClient.__init__(self, keys, nick, ident, name) self.factory = factory self.channels = {} self.users = {} self.factory.join = self.join self.factory.leave = self.leave self.factory.send = self.send def _create_event(self, type, user, channel): event = Event(self.factory.name, type) event.sender['connection'] = unicode("%s@%s" % (user.username, user.hostname), 'utf-8', 'replace') event.sender['nick'] = unicode(user.nickname, 'utf-8', 'replace') event.sender['connection'] = self._to_hex(user.user_id) event.sender['id'] = self._to_hex(user.fingerprint) if channel: event.channel = unicode(channel.channel_name, 'utf-8', 'replace') else: event.channel = event.sender['connection'] event.public = True self.users[event.sender['connection']] = user self.users[event.sender['id']] = user return event def _state_event(self, user, channel, action, kicker=None, message=None): event = self._create_event(u'state', user, channel) event.state = action if kicker: event.kicker = unicode(self._to_hex(kicker.user_id), 'utf-8', 'replace') if message: event.message = unicode(message, 'utf-8', 'replace') self.factory.log.debug(u"%s has been kicked from %s by %s (%s)", event.sender['id'], event.channel, event.kicker, event.message) else: self.factory.log.debug(u"%s has %s %s", event.sender['connection'], action, event.channel) ibid.dispatcher.dispatch(event).addCallback(self.respond) def _message_event(self, msgtype, user, channel, msg): event = self._create_event(msgtype, user, channel) event.message = unicode(msg, 'utf-8', 'replace') self.factory.log.debug(u"Received %s from %s in %s: %s", msgtype, event.sender['id'], event.channel, event.message) if not channel: event.addressed = True event.public = False else: event.public = True ibid.dispatcher.dispatch(event).addCallback(self.respond) def respond(self, event): for response in event.responses: self.send(response) def send(self, response): message = response['reply'] flags=0 if response.get('action', False): flags=4 if response['target'] in self.users: target = self.users[response['target']] self.send_private_message(target, message, flags=flags) elif response['target'] in self.channels: target = self.channels[response['target']] if response.get('topic', False): self.command_call('TOPIC %s %s' % (target, message)) else: self.send_channel_message(target, message, flags=flags) else: for user in self.users.itervalues(): if user.nickname == response['target']: self.send_private_message(user, message, flags=flags) return self.factory.log.debug(u"Unknown target: %s" % response['target']) return self.factory.log.debug(u"Sent message to %s: %s", response['target'], message) def logging_name(self, identity): format_user = lambda user: u'-'.join((user.nickname, self._to_hex(user.fingerprint))) if identity in self.users: user = self.users[identity] return format_user(user) if identity in self.channels: return identity # Only really used for saydo for user in self.users.itervalues(): if user.nickname == identity: return format_user(user) self.factory.log.error(u"Unknown identity: %s", identity) return identity def join(self, channel): self.command_call('JOIN %s' % channel) return True def leave(self, channel): if channel not in self.channels: return False self.command_call('LEAVE %s' % channel) del self.channels[channel] # TODO: When pysilc gets channel.user_list support # we should remove stale users return True def _to_hex(self, string): return u''.join(hex(ord(c)).replace('0x', '').zfill(2) for c in string) def channel_message(self, sender, channel, flags, message): self._message_event(u'message', sender, channel, message) def private_message(self, sender, flags, message): self._message_event(u'message', sender, None, message) def notify_join(self, user, channel): self._state_event(user, channel, u'online') def notify_leave(self, user, channel): self._state_event(user, channel, u'offline') def notify_signoff(self, user, channel): self._state_event(user, channel, u'offline') del self.users[self._to_hex(user.user_id)] del self.users[self._to_hex(user.fingerprint)] def notify_nick_change(self, user, old_nick, new_nick): event = self._create_event(u'state', user, None) event.state = u'offline' event.sender['nick'] = unicode(old_nick, 'utf-8', 'replace') event.othername = unicode(new_nick, 'utf-8', 'replace') ibid.dispatcher.dispatch(event).addCallback(self.respond) event = self._create_event(u'state', user, None) event.state = u'online' event.sender['nick'] = unicode(new_nick, 'utf-8', 'replace') event.othername = unicode(old_nick, 'utf-8', 'replace') ibid.dispatcher.dispatch(event).addCallback(self.respond) def notify_kicked(self, user, message, kicker, channel): self._state_event(user, channel, u'kicked', kicker, message) def notify_killed(self, user, message, kicker, channel): self._state_event(user, channel, u'killed', kicker, message) del self.users[self._to_hex(user.user_id)] del self.users[self._to_hex(user.fingerprint)] def running(self): self.connect_to_server(self.factory.server, self.factory.port) def connected(self): for channel in self.factory.channels: self.join(channel) event = Event(self.factory.name, u'source') event.status = u'connected' ibid.dispatcher.dispatch(event) def command_reply_join(self, channel, name, topic, hmac, x, y, users): self.channels[name] = channel for user in users: self._state_event(user, channel, u'online') def disconnect(self): self.command_call('QUIT') def disconnected(self, message): self.factory.log.info(u"Disconnected (%s)", message) event = Event(self.factory.name, u'source') event.status = u'disconnected' ibid.dispatcher.dispatch(event) self.factory.s.stopService() self.channels.clear() self.users.clear() def failure(self): self.factory.log.error(u'Connection failure') class SourceFactory(IbidSourceFactory): auth = ('implicit',) supports = ('action', 'topic') server = Option('server', 'Server hostname') port = IntOption('port', 'Server port number', 706) nick = Option('nick', 'Nick', ibid.config['botname']) channels = ListOption('channels', 'Channels to autojoin', []) realname = Option('realname', 'Real Name', ibid.config['botname']) public_key = Option('public_key', 'Filename of public key', 'silc.pub') private_key = Option('private_key', 'Filename of private key', 'silc.prv') max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % self.name) pub = join(ibid.options['base'], self.public_key) prv = join(ibid.options['base'], self.private_key) if not exists(pub) and not exists(prv): keys = create_key_pair(pub, prv, passphrase='') else: keys = load_key_pair(pub, prv, passphrase='') self.client = SilcBot(keys, self.nick, self.nick, self.realname, self) def run_one(self): self.client.run_one() def setServiceParent(self, service): self.s = internet.TimerService(0.2, self.run_one) if service is None: self.s.startService() else: self.s.setServiceParent(service) def disconnect(self): self.client.disconnect() return True def url(self): return u'silc://%s@%s:%s' % (self.nick, self.server, self.port) def logging_name(self, identity): return self.client.logging_name(identity) def truncation_point(self, response, event=None): if response.get('target', None) in self.client.channels: return self.max_public_message_length return None # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/jabber.py0000644000000000000000000002726211405214700015127 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from wokkel import client, xmppim, subprotocols from twisted.internet import protocol, reactor, ssl from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish import ibid from ibid.config import Option, BoolOption, IntOption, ListOption from ibid.source import IbidSourceFactory from ibid.event import Event class Message(domish.Element): def __init__(self, to, frm, body, type): domish.Element(self, (None, 'message')) self['to'] = to self['from'] = frm self['type'] = 'chat' self.addElement('body', content=body) class JabberBot(xmppim.MessageProtocol, xmppim.PresenceClientProtocol, xmppim.RosterClientProtocol): def __init__(self): xmppim.MessageProtocol.__init__(self) self.rooms = [] self.room_users = {} def connectionInitialized(self): self.parent.log.info(u"Connected") xmppim.MessageProtocol.connectionInitialized(self) xmppim.PresenceClientProtocol.connectionInitialized(self) xmppim.RosterClientProtocol.connectionInitialized(self) self.xmlstream.send(xmppim.AvailablePresence()) self.roster = self.getRoster() #See section 7.3 of http://www.ietf.org/rfc/rfc3921.txt self.name = self.parent.name self.parent.send = self.send self.parent.proto = self for room in self.parent.rooms: self.join(room) event = Event(self.parent.name, u'source') event.status = u'connected' ibid.dispatcher.dispatch(event) def connectionLost(self, reason): self.parent.log.info(u"Disconnected (%s)", reason) event = Event(self.parent.name, u'source') event.status = u'disconnected' ibid.dispatcher.dispatch(event) subprotocols.XMPPHandler.connectionLost(self, reason) def _state_event(self, entity, state, realjid=None): event = Event(self.name, u'state') event.state = state if entity.userhost().lower() in self.rooms: nick = entity.full().split('/')[1] event.channel = entity.userhost() if nick == self.parent.nick: event.type = u'connection' event.status = state == u'online' and u'joined' or u'left' else: if realjid: if state == u'online': self.room_users[entity.full()] = realjid.full() elif state == u'offline': if entity.full() in self.room_users: del self.room_users[entity.full()] event.sender['connection'] = realjid.full() event.sender['id'] = realjid.userhost() else: event.sender['connection'] = entity.full() event.sender['id'] = event.sender['connection'] event.sender['nick'] = nick event.public = True else: event.sender['connection'] = entity.full() event.sender['id'] = event.sender['connection'].split('/')[0] event.sender['nick'] = event.sender['connection'].split('@')[0] event.channel = entity.full() event.public = False ibid.dispatcher.dispatch(event).addCallback(self.respond) def _onPresenceAvailable(self, presence): entity = JID(presence["from"]) show = unicode(presence.show or '') if show not in ['away', 'xa', 'chat', 'dnd']: show = None statuses = self._getStatuses(presence) try: priority = int(unicode(presence.priority or '')) or 0 except ValueError: priority = 0 realjid = None for mucuser in presence.elements(name='x', uri='http://jabber.org/protocol/muc#user'): if mucuser.item.hasAttribute('jid'): realjid = JID(mucuser.item["jid"]) self.availableReceived(entity, show, statuses, priority, realjid) def _onPresenceUnavailable(self, presence): entity = JID(presence["from"]) statuses = self._getStatuses(presence) realjid = None if presence.x and presence.x.defaultUri == 'http://jabber.org/protocol/muc#user' and presence.x.item.hasAttribute('jid'): realjid = JID(presence.x.item["jid"]) self.unavailableReceived(entity, statuses, realjid) def availableReceived(self, entity, show=None, statuses=None, priority=0, realjid=None): self.parent.log.debug(u"Received available presence from %s (actually %s) (%s)", entity.full(), realjid and realjid.full() or None, show) self._state_event(entity, u'online', realjid) def unavailableReceived(self, entity, statuses, realjid=None): self.parent.log.debug(u"Received unavailable presence from %s", entity.full()) self._state_event(entity, u'offline', realjid) def subscribeReceived(self, entity): response = xmppim.Presence(to=entity, type='subscribed') self.xmlstream.send(response) response = xmppim.Presence(to=entity, type='subscribe') self.xmlstream.send(response) self.parent.log.info(u"Received and accepted subscription request from %s", entity.full()) def onMessage(self, message): self.parent.log.debug(u"Received %s message from %s: %s", message['type'], message['from'], message.body) if message.x and message.x.defaultUri == 'jabber:x:delay': self.parent.log.debug(u"Ignoring delayed message") return if self.parent.accept_domains: if message['from'].split('/')[0].split('@')[1] not in self.parent.accept_domains: self.parent.log.info(u"Ignoring message because sender is not in accept_domains") return if message.body is None: self.parent.log.info(u'Ignoring empty message') return event = Event(self.parent.name, u'message') event.message = unicode(message.body) event.sender['connection'] = message['from'] if message['type'] == 'groupchat': if message['from'] in self.room_users: event.sender['connection'] = self.room_users[message['from']] event.sender['id'] = event.sender['connection'].split('/')[0] else: event.sender['id'] = message['from'] if event.sender['id'] == self.parent.nick: return if '/' in message['from']: event.sender['nick'] = message['from'].split('/')[1] else: event.sender['nick'] = message['from'] event.channel = message['from'].split('/')[0] event.public = True else: event.sender['id'] = event.sender['connection'].split('/')[0] event.sender['nick'] = event.sender['connection'].split('@')[0] event.channel = event.sender['connection'] event.public = False event.addressed = True ibid.dispatcher.dispatch(event).addCallback(self.respond) def respond(self, event): for response in event.responses: self.send(response) def send(self, response): message = domish.Element((None, 'message')) message['to'] = response['target'] message['from'] = self.parent.authenticator.jid.full() if message['to'] in self.rooms: message['type'] = 'groupchat' else: message['type'] = 'chat' message.addElement('body', content=response['reply']) self.xmlstream.send(message) self.parent.log.debug(u"Sent %s message to %s: %s", message['type'], message['to'], message.body) def join(self, room): self.parent.log.info(u"Joining %s", room) jid = JID('%s/%s' % (room, self.parent.nick)) presence = xmppim.AvailablePresence(to=jid) self.xmlstream.send(presence) self.rooms.append(room.lower()) def leave(self, room): self.parent.log.info(u"Leaving %s", room) jid = JID('%s/%s' % (room, self.parent.nick)) presence = xmppim.UnavailablePresence(to=jid) self.xmlstream.send(presence) self.rooms.remove(room.lower()) class IbidXMPPClientConnector(client.XMPPClientConnector): def __init__(self, reactor, domain, factory, server, port, ssl): client.XMPPClientConnector.__init__(self, reactor, domain, factory) self.overridden_server = server self.overridden_port = port self.overridden_ssl = ssl def pickServer(self): srvhost, srvport = client.XMPPClientConnector.pickServer(self) host, port = self.overridden_server, self.overridden_port if host is None: host = srvhost if self.overridden_ssl: if port is None: port = 5223 self.connectFuncName = 'connectSSL' self.connectFuncArgs = [ssl.ClientContextFactory()] if port is None: port = srvport self.factory.log.info(u'Connecting to: %s:%s%s', host, port, self.overridden_ssl and ' using SSL' or '') return host, port def connectionFailed(self, reason): self.factory.log.error(u'Connection failed: %s', reason) self.factory.clientConnectionFailed(self, reason) def connectionLost(self, reason): self.factory.log.error(u'Connection lost: %s', reason) self.factory.clientConnectionLost(self, reason) class SourceFactory(client.DeferredClientFactory, protocol.ReconnectingClientFactory, IbidSourceFactory): auth = ('implicit',) supports = ('multiline',) jid_str = Option('jid', 'Jabber ID') server = Option('server', 'Server hostname (defaults to SRV lookup, ' 'falling back to JID domain)') port = IntOption('port', 'Server port number (defaults to SRV lookup, ' 'falling back to 5222/5223') ssl = BoolOption('ssl', 'Use SSL instead of automatic TLS') password = Option('password', 'Jabber password') nick = Option('nick', 'Nick for chatrooms', ibid.config['botname']) rooms = ListOption('rooms', 'Chatrooms to autojoin', []) accept_domains = ListOption('accept_domains', 'Only accept messages from these domains', []) max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) client.DeferredClientFactory.__init__(self, JID(self.jid_str), self.password) bot = JabberBot() self.addHandler(bot) bot.setHandlerParent(self) def setServiceParent(self, service): c = IbidXMPPClientConnector(reactor, self.authenticator.jid.host, self, self.server, self.port, self.ssl) c.connect() def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() self.proto.xmlstream.transport.loseConnection() return True def join(self, room): return self.proto.join(room) def leave(self, room): return self.proto.leave(room) def url(self): return u'xmpp://%s' % (self.jid_str,) def logging_name(self, identity): return identity.split('/')[0] def truncation_point(self, response, event=None): if response.get('target', None) in self.proto.rooms: return self.max_public_message_length return None # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/source/manhole.py0000644000000000000000000000222611362435477015340 0ustar rootroot# Copyright (c) 2008-2009, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from twisted.internet import reactor from twisted.application import internet from twisted.manhole.telnet import ShellFactory from ibid.source import IbidSourceFactory from ibid.config import Option, IntOption class SourceFactory(ShellFactory, IbidSourceFactory): port = IntOption('port', 'Port number to listen on', 9898) username = Option('username', 'Login Username', 'admin') password = Option('password', 'Login Password', 'admin') def __init__(self, name): ShellFactory.__init__(self) IbidSourceFactory.__init__(self, name) self.name = name def setServiceParent(self, service=None): if service: self.listener = internet.TCPServer(self.port, self).setServiceParent(service) return self.listener else: self.listener = reactor.listenTCP(self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.listener.stopListening() return True # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/__init__.py0000644000000000000000000001266511362435477014164 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging import logging.config from os import makedirs from os.path import join, dirname, expanduser, exists import sys from threading import Lock from ConfigParser import SafeConfigParser sys.path.insert(0, '%s/lib' % dirname(__file__)) import twisted.python.log import ibid.core from ibid.compat import defaultdict from ibid.config import FileConfig class InsensitiveDict(dict): def __getitem__(self, key): return dict.__getitem__(self, key.lower()) def __setitem__(self, key, value): dict.__setitem__(self, key.lower(), value) def __contains__(self, key): return dict.__contains__(self, key.lower()) ms_log = logging.getLogger('core.channel_tracking') class MultiSet(object): """Multi-Set for channel member tracking. Allows out-of-order updates. Atomic: add, remove, discard There are no other guarantees. """ __slots__ = ['lock', '_dict'] def __init__(self): self.lock = Lock() self._dict = {} def add(self, value): self.lock.acquire() if value in self._dict and self._dict[value] == -1: del self._dict[value] else: self._dict[value] = self._dict.get(value, 0) + 1 if self._dict[value] > 2: ms_log.warning(u'High value in multi-set: %s: %s', repr(value), repr(self._dict[value])) self.lock.release() def remove(self, value): self.lock.acquire() if value in self._dict and self._dict[value] == 1: del self._dict[value] else: self._dict[value] = self._dict.get(value, 0) - 1 if self._dict[value] < -1: ms_log.warning(u'Low value in multi-set: %s: %s', repr(value), repr(self._dict[value])) self.lock.release() def discard(self, value): self.lock.acquire() if value in self._dict: del self._dict[value] self.lock.release() def __contains__(self, value): return self._dict.get(value, 0) > 0 def __iter__(self): for item in self._dict.iterkeys(): if self._dict.get(item, 0) > 0: yield item def __repr__(self): return self._dict.__repr__() sources = InsensitiveDict() config = {} dispatcher = None processors = [] categories = {} reloader = None databases = {} auth = None service = None options = { 'base': '.', } rpc = {} channels = defaultdict(lambda: defaultdict(MultiSet)) def twisted_log(eventDict): log = logging.getLogger('twisted') if 'failure' in eventDict: log.error(eventDict.get('why') or 'Unhandled exception' + '\n' + str(eventDict['failure'].getTraceback())) elif 'warning' in eventDict: log.warning(eventDict['warning']) else: log.debug(' '.join([str(m) for m in eventDict['message']])) def setup(opts, service=None): service = service for key, value in opts.items(): options[key] = value options['base'] = dirname(options['config']) sys.path.insert(0, options['base']) # Get Twisted to log to Python logging twisted.python.log.startLoggingWithObserver(twisted_log) # Undo Twisted logging's redirection of stdout and stderr sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ logging.basicConfig(level=logging.INFO) if not exists(options['config']): raise IbidException('Cannot find configuration file %s' % options['config']) ibid.config = FileConfig(options['config']) ibid.config.merge(FileConfig(join(options['base'], 'local.ini'))) if 'logging' in ibid.config: logging.getLogger('core').info(u'Loading log configuration from %s', ibid.config['logging']) create_logdirs(ibid.config['logging']) logging.config.fileConfig(join(options['base'], expanduser(ibid.config['logging']))) ibid.reload_reloader() ibid.reloader.reload_dispatcher() ibid.reloader.reload_databases() ibid.reloader.load_processors() ibid.reloader.load_sources(service) ibid.reloader.reload_auth() def reload_reloader(): try: reload(ibid.core) new_reloader = ibid.core.Reloader() ibid.reloader = new_reloader return True except: logging.getLogger('core').exception(u"Exception occured while reloading Reloader") return False def create_logdirs(configfile): config = SafeConfigParser() config.read(configfile) if config.has_option('handlers', 'keys'): handlers = config.get('handlers', 'keys').split(',') for handler in handlers: section = 'handler_' + handler if config.has_option(section, 'class') and config.get(section, 'class') in ('FileHandler', 'handlers.RotatingFileHandler', 'handlers.TimedRotatingFileHandler'): if config.has_option(section, 'args'): try: args = eval(config.get(section, 'args')) except Exception: continue if isinstance(args, tuple) and len(args) > 0: dir = dirname(args[0]) if not exists(dir): makedirs(dir) class IbidException(Exception): pass class AuthException(IbidException): pass class SourceException(IbidException): pass # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/config.py0000644000000000000000000000510411362435477013660 0ustar rootroot# Copyright (c) 2008-2009, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging from configobj import ConfigObj from validate import Validator import ibid from ibid.utils import locate_resource def monkeypatch(self, name): if self.has_key(name): return self[name] super(ConfigObj, self).__getattr__(name) ConfigObj.__getattr__ = monkeypatch def FileConfig(filename): spec = file(locate_resource('ibid', 'configspec.ini'), 'r') configspec = ConfigObj(spec, list_values=False, encoding='utf-8') config = ConfigObj(filename, configspec=configspec, interpolation='Template', encoding='utf-8') config.validate(Validator()) logging.getLogger('core.config').info(u"Loaded configuration from %s", filename) return config class Option(object): accessor = 'get' def __init__(self, name, description, default=None): self.name = name self.default = default self.description = description __import__('ibid.plugins') __import__('ibid.source') def __get__(self, instance, owner): if instance is None: return self.default if issubclass(owner, ibid.plugins.Processor): config = ibid.config.plugins elif issubclass(owner, ibid.source.IbidSourceFactory): config = ibid.config.sources else: raise AttributeError if instance.name in config and self.name in config[instance.name]: section = config[instance.name] return getattr(section, self.accessor)(self.name) else: return self.default class BoolOption(Option): accessor = 'as_bool' class IntOption(Option): accessor = 'as_int' class FloatOption(Option): accessor = 'as_float' class ListOption(Option): def __get__(self, instance, owner): value = Option.__get__(self, instance, owner) if not isinstance(value, (list, tuple)): value = [value] if value and not value[0] and self.default: both = [] both.extend(self.default) both.extend(value[1:]) value = both return value class DictOption(Option): def __get__(self, instance, owner): value = Option.__get__(self, instance, owner) if self.default and value is not self.default: both = self.default.copy() both.update(value) value = both for k, v in value.items(): if not v: del value[k] return value # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/compat.py0000644000000000000000000000512511362435477013701 0ustar rootroot# Copyright (c) 2009, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. """ Compatibility functions for older versions of Python. We support 2.4 <= x < 3. Use this instead of: * all * any * collections.defaultdict * datetime.strptime * email.utils * hashlib * (simple)json * math.factorial * xml.etree (cElementTree) """ from sys import version_info as _version_info _minor = _version_info[1] if _minor >= 5: from collections import defaultdict from datetime import datetime as _datetime dt_strptime = _datetime.strptime import email.utils as email_utils import hashlib from xml.etree import cElementTree as ElementTree all = all any = any else: import cElementTree as ElementTree import email.Utils as email_utils def all(iterable): for element in iterable: if not element: return False return True def any(iterable): for element in iterable: if element: return True return False class defaultdict(dict): def __init__(self, default_factory=None, *rest): dict.__init__(self, *rest) self.default_factory = default_factory def __missing__(self, key): if self.default_factory is None: raise KeyError(key) value = self.default_factory() dict.__setitem__(self, key, value) return value def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return self.__missing__(key) import md5 as _md5 import sha as _sha class hashlib(object): @staticmethod def md5(x): return _md5.new(x) @staticmethod def sha1(x): return _sha.new(x) @staticmethod def sha224(x): class unsupported(object): @staticmethod def hexdigest(): return 'Not Supported' return unsupported sha512 = sha384 = sha224 from datetime import datetime as _datetime import time as _time def dt_strptime(date_string, format): return _datetime(*(_time.strptime(date_string, format)[:6])) if _minor >= 6: import json from math import factorial else: import simplejson as json def factorial(x): if not isinstance(x, int) or x < 0: raise ValueError if x == 0: return 1 return reduce(lambda a, b: a * b, xrange(1, x + 1)) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/static/0000755000000000000000000000000011531277655013330 5ustar rootrootibid-0.1.1/ibid/static/PLACEHOLDER0000644000000000000000000000011311362435477014770 0ustar rootrootThis directory will eventually contain static content for the http source. ibid-0.1.1/ibid/plugins/0000755000000000000000000000000011531277655013522 5ustar rootrootibid-0.1.1/ibid/plugins/strings.py0000644000000000000000000001336311414117331015553 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera, Russell Cloran, # Adrian Moisey # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from crypt import crypt import base64 import re from ibid.compat import hashlib from ibid.plugins import Processor, match, authorise features = {} features['hash'] = { 'description': u'Calculates numerous cryptographic hash functions.', 'categories': ('calculate',), } class Hash(Processor): usage = u"""(md5|sha1|sha224|sha256|sha384|sha512) crypt """ feature = ('hash',) @match(r'^(md5|sha1|sha224|sha256|sha384|sha512)(?:sum)?\s+(.+?)$') def hash(self, event, hash, string): func = getattr(hashlib, hash.lower()) event.addresponse(unicode(func(string.encode('utf-8')).hexdigest())) @match(r'^crypt\s+(.+)\s+(\S+)$') def handle_crypt(self, event, string, salt): event.addresponse(unicode(crypt(string.encode('utf-8'), salt.encode('utf-8')))) features['base64'] = { 'description': u'Encodes and decodes base 16, 32 and 64. Assumes UTF-8.', 'categories': ('calculate', 'convert', 'development',), } class Base64(Processor): usage = u'base(16|32|64) (encode|decode) ' feature = ('base64',) @match(r'^b(?:ase)?(16|32|64)\s*(enc|dec)(?:ode)?\s+(.+?)$') def base64(self, event, base, operation, string): operation = operation.lower() func = getattr(base64, 'b%s%sode' % (base, operation)) if operation == 'dec': try: bytes = func(string) event.addresponse(u"Assuming UTF-8: '%s'", unicode(bytes, 'utf-8', 'strict')) except TypeError, e: event.addresponse(u"Invalid base%(base)s: %(error)s", {'base': base, 'error': unicode(e)}) except UnicodeDecodeError: event.addresponse(u'Not UTF-8: %s', unicode(repr(bytes))) else: event.addresponse(unicode(func(string.encode('utf-8')))) features['rot13'] = { 'description': u'Transforms a string with ROT13.', 'categories': ('convert', 'fun',), } class Rot13(Processor): usage = u'rot13 ' feature = ('rot13',) @match(r'^rot13\s+(.+)$') def rot13(self, event, string): repl = lambda x: x.group(0).encode('rot13') event.addresponse(re.sub('[a-zA-Z]+', repl, string)) features['dvorak'] = { 'description': u'Makes text typed on a QWERTY keyboard as if it was Dvorak work, and vice-versa', 'categories': ('convert', 'fun',), } class Dvorak(Processor): usage = u"""(aoeu|asdf) """ feature = ('dvorak',) # List of characters on each keyboard layout dvormap = u"""',.pyfgcrl/=aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+AOEUIDHTNS_:QJKXBMWVZ[]{}|""" qwermap = u"""qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?-=_+|""" # Typed by a QWERTY typist on a Dvorak-mapped keyboard typed_on_dvorak = dict(zip(map(ord, dvormap), qwermap)) # Typed by a Dvorak typist on a QWERTY-mapped keyboard typed_on_qwerty = dict(zip(map(ord, qwermap), dvormap)) @match(r'^(?:asdf|dvorak)\s+(.+)$') def convert_from_qwerty(self, event, text): event.addresponse(text.translate(self.typed_on_qwerty)) @match(r'^(?:aoeu|qwerty)\s+(.+)$') def convert_from_dvorak(self, event, text): event.addresponse(text.translate(self.typed_on_dvorak)) features['retest'] = { 'description': u'Checks whether a regular expression matches a given ' u'string.', 'categories': ('development',), } class ReTest(Processor): usage = u'does match ' feature = ('retest',) permission = 'regex' @match('^does\s+(.+?)\s+match\s+(.+?)$') @authorise(fallthrough=False) def retest(self, event, regex, string): event.addresponse(re.search(regex, string) and u'Yes' or u'No') features['morse'] = { 'description': u'Translates messages into and out of morse code.', 'categories': ('convert', 'fun',), } class Morse(Processor): usage = u'morse (text|morsecode)' feature = ('morse',) _table = { 'A': ".-", 'B': "-...", 'C': "-.-.", 'D': "-..", 'E': ".", 'F': "..-.", 'G': "--.", 'H': "....", 'I': "..", 'J': ".---", 'K': "-.-", 'L': ".-..", 'M': "--", 'N': "-.", 'O': "---", 'P': ".--.", 'Q': "--.-", 'R': ".-.", 'S': "...", 'T': "-", 'U': "..-", 'V': "...-", 'W': ".--", 'X': "-..-", 'Y': "-.--", 'Z': "--..", '0': "-----", '1': ".----", '2': "..---", '3': "...--", '4': "....-", '5': ".....", '6': "-....", '7': "--...", '8': "---..", '9': "----.", ' ': " ", '.': ".-.-.-", ',': "--..--", '?': "..--..", ':': "---...", ';': "-.-.-.", '-': "-....-", '_': "..--.-", '"': ".-..-.", "'": ".----.", '/': "-..-.", '(': "-.--.", ')': "-.--.-", '=': "-...-", } _rtable = dict((v, k) for k, v in _table.items()) def _text2morse(self, text): return u" ".join(self._table.get(c.upper(), c) for c in text) def _morse2text(self, morse): toks = morse.split(u' ') return u"".join(self._rtable.get(t, u' ') for t in toks) @match(r'^morse\s*(.*)$', 'deaddressed') def morse(self, event, message): if not (set(message) - set(u'-./ \t\n')): event.addresponse(u'Decodes as %s', self._morse2text(message)) else: event.addresponse(u'Encodes as %s', self._text2morse(message)) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/google.py0000644000000000000000000001224711530301254015334 0ustar rootroot# Copyright (c) 2008-2011, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from httplib import BadStatusLine import re from urllib import urlencode from ibid.compat import ElementTree from ibid.config import Option from ibid.plugins import Processor, match from ibid.utils import decode_htmlentities, json_webservice from ibid.utils.html import get_html_parse_tree features = {'google': { 'description': u'Retrieves results from Google and Google Calculator.', 'categories': ('lookup', 'web', 'calculate', ), }} default_user_agent = 'Mozilla/5.0' default_referer = "http://ibid.omnia.za.net/" class GoogleAPISearch(Processor): usage = u"""google[.] [for] googlefight [for] and """ feature = ('google',) api_key = Option('api_key', 'Your Google API Key (optional)', None) referer = Option('referer', 'The referer string to use (API searches)', default_referer) def _google_api_search(self, query, resultsize="large", country=None): params = { 'v': '1.0', 'q': query, 'rsz': resultsize, } if country is not None: params['gl'] = country if self.api_key: params['key'] = self.api_key headers = {'referer': self.referer} return json_webservice('http://ajax.googleapis.com/ajax/services/search/web', params, headers) @match(r'^google(?:\.com?)?(?:\.([a-z]{2}))?\s+(?:for\s+)?(.+?)$') def search(self, event, country, query): try: items = self._google_api_search(query, country=country) except BadStatusLine: event.addresponse(u"Google appears to be broken (or more likely, my connection to it)") return results = [] for item in items["responseData"]["results"]: title = item["titleNoFormatting"] results.append(u'"%s" %s' % (decode_htmlentities(title), item["unescapedUrl"])) if results: event.addresponse(u' :: '.join(results)) else: event.addresponse(u"Wow! Google couldn't find anything") @match(r'^(?:rank|(?:google(?:fight|compare|cmp)))\s+(?:for\s+)?(.+?)\s+and\s+(.+?)$') def googlefight(self, event, term1, term2): try: count1 = int(self._google_api_search(term1, "small")["responseData"]["cursor"].get("estimatedResultCount", 0)) count2 = int(self._google_api_search(term2, "small")["responseData"]["cursor"].get("estimatedResultCount", 0)) except BadStatusLine: event.addresponse(u"Google appears to be broken (or more likely, my connection to it)") return event.addresponse(u'%(firstterm)s wins with %(firsthits)i hits, %(secondterm)s had %(secondhits)i hits', (count1 > count2 and { 'firstterm': term1, 'firsthits': count1, 'secondterm': term2, 'secondhits': count2, } or { 'firstterm': term2, 'firsthits': count2, 'secondterm': term1, 'secondhits': count1, })) # Unfortunatly google API search doesn't support all of google search's # features. # Dear Google: We promise we don't bite. class GoogleScrapeSearch(Processor): usage = u"""gcalc gdefine """ feature = ('google',) user_agent = Option('user_agent', 'HTTP user agent to present to Google (for non-API searches)', default_user_agent) def _google_scrape_search(self, query, country=None): params = {'q': query.encode('utf-8')} if country: params['cr'] = u'country' + country.upper() return get_html_parse_tree( 'http://www.google.com/search?' + urlencode(params), headers={'user-agent': self.user_agent}, treetype='etree') @match(r'^gcalc\s+(.+)$') def calc(self, event, expression): tree = self._google_scrape_search(expression) nodes = [node for node in tree.findall('.//h2/b')] if len(nodes) == 1: # ElementTree doesn't support inline tags: # May return ASCII unless an encoding is specified. # "utf8" will result in an xml header node = ElementTree.tostring(nodes[0], encoding='utf-8') node = node.decode('utf-8') node = re.sub(r'(.*?)', lambda x: u'^' + x.group(1), node) node = re.sub(r'<.*?>', '', node) node = re.sub(r'(\d)\s+(\d)', lambda x: x.group(1) + x.group(2), node) node = decode_htmlentities(node) event.addresponse(node) else: event.addresponse(u'No result') @match(r'^gdefine\s+(.+)$') def define(self, event, term): tree = self._google_scrape_search("define:%s" % term) definitions = [] for li in tree.findall('.//li'): if li.text: definitions.append(li.text) if definitions: event.addresponse(u' :: '.join(definitions)) else: event.addresponse(u'Are you making up words again?') # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/factoid.py0000644000000000000000000010334711531035617015503 0ustar rootroot# Copyright (c) 2009-2011, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import datetime import logging from random import choice import re from dateutil.tz import tzlocal, tzutc from ibid.plugins import Processor, match, handler, authorise, auth_responses, \ RPC from ibid.config import Option, IntOption, ListOption from ibid.db import IbidUnicode, IbidUnicodeText, Boolean, Integer, DateTime, \ Table, Column, ForeignKey, PassiveDefault, \ relation, synonym, func, or_, \ Base, VersionedSchema, \ get_regexp_op from ibid.plugins.identity import get_identities from ibid.utils import format_date features = {'factoid': { 'description': u'Factoids are arbitrary pieces of information stored by a ' u'key. Factoids beginning with a command such as "" ' u'or "" will supress the "name verb value" output. ' u"Search and replace functions won't use real regexs unless " u"appended with the 'r' flag.", 'categories': ('lookup', 'remember',), }} log = logging.getLogger('plugins.factoid') default_verbs = ('is', 'are', 'has', 'have', 'was', 'were', 'do', 'does', 'can', 'should', 'would') default_interrogatives = ('what', 'wtf', 'where', 'when', 'who', "what's", "who's") def strip_name(unstripped): "Apply to factoid names, as we use unstripped matches" return re.match(r'^\s*(.*?)\s*[?!.]*\s*$', unstripped, re.DOTALL).group(1) def escape_name(name): "Turn a $arg factoid name to _%" return name.replace('%', '\\%').replace('_', '\\_').replace('$arg', '_%') def unescape_name(name): "Turn a _% factoid name to $arg" return name.replace('_%', '$arg').replace('\\%', '%').replace('\\_', '_') class FactoidName(Base): __table__ = Table('factoid_names', Base.metadata, Column('id', Integer, primary_key=True), Column('name', IbidUnicodeText(32, case_insensitive=True), key='_name', nullable=False, unique=True, index=True), Column('factoid_id', Integer, ForeignKey('factoids.id'), nullable=False, index=True), Column('identity_id', Integer, ForeignKey('identities.id'), index=True), Column('time', DateTime, nullable=False), Column('factpack', Integer, ForeignKey('factpacks.id'), index=True), Column('wild', Boolean, nullable=False, default=False, index=True), useexisting=True) class FactoidNameSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_column(Column('factpack', Integer, ForeignKey('factpacks.id'))) def upgrade_2_to_3(self): self.add_index(self.table.c.name) def upgrade_3_to_4(self): self.add_index(self.table.c.name) self.add_index(self.table.c.factoid_id) self.add_index(self.table.c.identity_id) self.add_index(self.table.c.factpack) def upgrade_4_to_5(self): self.alter_column(Column('name', IbidUnicode(64), key='_name', nullable=False, unique=True, index=True)) def upgrade_5_to_6(self): self.alter_column(Column('name', IbidUnicodeText(32), key='_name', nullable=False, unique=True, index=True)) def upgrade_6_to_7(self): self.add_column(Column('wild', Boolean, PassiveDefault('0'), nullable=False, index=True, default=False)) for row in self.upgrade_session.query(FactoidName) \ .filter(FactoidName.name.like('%#_#%%', escape='#')) \ .all(): row.wild = True self.upgrade_session.save_or_update(row) def upgrade_7_to_8(self): self.drop_index(self.table.c._name) self.alter_column(Column('name', IbidUnicodeText(32, case_insensitive=True), key='_name', nullable=False, unique=True, index=True), force_rebuild=True) self.add_index(self.table.c._name) __table__.versioned_schema = FactoidNameSchema(__table__, 8) def __init__(self, name, identity_id, factoid_id=None, factpack=None): self.name = name self.factoid_id = factoid_id self.identity_id = identity_id self.time = datetime.utcnow() self.factpack = factpack def __repr__(self): return u'' % (self.name, self.factoid_id) def _get_name(self): return unescape_name(self._name) def _set_name(self, name): self.wild = u'$arg' in name self._name = escape_name(name) name = synonym('_name', descriptor=property(_get_name, _set_name)) class FactoidValue(Base): __table__ = Table('factoid_values', Base.metadata, Column('id', Integer, primary_key=True), Column('value', IbidUnicodeText, nullable=False), Column('factoid_id', Integer, ForeignKey('factoids.id'), nullable=False, index=True), Column('identity_id', Integer, ForeignKey('identities.id'), index=True), Column('time', DateTime, nullable=False), Column('factpack', Integer, ForeignKey('factpacks.id'), index=True), useexisting=True) class FactoidValueSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_column(Column('factpack', Integer, ForeignKey('factpacks.id'))) def upgrade_2_to_3(self): self.add_index(self.table.c.factoid_id) self.add_index(self.table.c.identity_id) self.add_index(self.table.c.factpack) def upgrade_3_to_4(self): self.alter_column(Column('value', IbidUnicodeText, nullable=False), force_rebuild=True) __table__.versioned_schema = FactoidValueSchema(__table__, 4) def __init__(self, value, identity_id, factoid_id=None, factpack=None): self.value = value self.factoid_id = factoid_id self.identity_id = identity_id self.time = datetime.utcnow() self.factpack = factpack def __repr__(self): return u'' % (self.factoid_id, self.value) class Factoid(Base): __table__ = Table('factoids', Base.metadata, Column('id', Integer, primary_key=True), Column('time', DateTime, nullable=False), Column('factpack', Integer, ForeignKey('factpacks.id'), index=True), useexisting=True) names = relation(FactoidName, cascade='all,delete', backref='factoid') values = relation(FactoidValue, cascade='all,delete', backref='factoid') class FactoidSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_column(Column('factpack', Integer, ForeignKey('factpacks.id'))) def upgrade_2_to_3(self): self.add_index(self.table.c.factpack) __table__.versioned_schema = FactoidSchema(__table__, 3) def __init__(self, factpack=None): self.time = datetime.utcnow() self.factpack = factpack def __repr__(self): return u"" % (', '.join([name.name for name in self.names]), ', '.join([value.value for value in self.values])) class Factpack(Base): __table__ = Table('factpacks', Base.metadata, Column('id', Integer, primary_key=True), Column('name', IbidUnicode(64, case_insensitive=True), nullable=False, unique=True, index=True), useexisting=True) class FactpackSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.name) def upgrade_2_to_3(self): self.drop_index(self.table.c.name) self.alter_column(Column('name', IbidUnicode(64, case_insensitive=True), nullable=False, unique=True, index=True), force_rebuild=True) self.add_index(self.table.c.name) __table__.versioned_schema = FactpackSchema(__table__, 3) def __init__(self, name): self.name = name def __repr__(self): return u'' % (self.name,) action_re = re.compile(r'^\s*\s*') reply_re = re.compile(r'^\s*\s*') escape_like_re = re.compile(r'([%_#])') def get_factoid(session, name, number, pattern, is_regex, all=False, literal=False): """session: SQLAlchemy session name: Factoid name (can contain arguments unless literal query) number: If not None, restrict to factoid[number] (or factoid[number:] for literal queries) pattern: If not None, restrict to factoids matching this pattern (cannot be used in conjuction with number) is_regex: Pattern is a real regex all: Return a random factoid from the set if False literal: Match factoid name literally (implies all) if all or literal, a list is returned; otherwise a single factoid is returned if nothing is found, either an empty list or None is returned """ assert not (number and pattern), u'number and pattern cannot be used together' if literal: all = True # as mentioned in the docstring # First pass for exact matches, if necessary again for wildcard matches if literal: passes = (False,) else: passes = (False, True) for wild in passes: factoid = None query = session.query(Factoid)\ .add_entity(FactoidName).join(Factoid.names)\ .add_entity(FactoidValue).join(Factoid.values) if wild: # Reversed LIKE because factoid name contains SQL wildcards if # factoid supports arguments query = query.filter( 'lower(:fact) LIKE lower(name) ESCAPE :escape' ).params(fact=name, escape='\\') else: query = query.filter(FactoidName.name == escape_name(name)) # For normal matches, restrict to the subset applicable if not literal: query = query.filter(FactoidName.wild == wild) if pattern: if is_regex: op = get_regexp_op(session) query = query.filter(op(FactoidValue.value, pattern)) else: pattern = '%%%s%%' % escape_like_re.sub(r'#\1', pattern) query = query.filter(func.lower(FactoidValue.value) .like(pattern, escape='#')) if number is not None: number = max(0, int(number) - 1) try: # The .all() is to ensure the result is a list. # It gets len()ed later if literal: return query.order_by(FactoidValue.id).all()[number:] else: factoid = query.order_by(FactoidValue.id).all()[number] except IndexError: continue if all: if factoid is not None: return [factoid] else: factoid = query.all() else: factoid = factoid or query.order_by(func.random()).first() if factoid: return factoid if all: return [] return None class Utils(Processor): usage = u'literal [( # | //[r] )]' feature = ('factoid',) @match(r'^literal\s+(.+?)(?:\s+#(\d+)|\s+(?:/(.+?)/(r?)))?$') def literal(self, event, name, number, pattern, is_regex): factoids = get_factoid(event.session, name, number, pattern, is_regex, literal=True) number = number and int(number) or 1 if factoids: event.addresponse(u', '.join(u'%i: %s' % (index + number, value.value) for index, (factoid, name, value) in enumerate(factoids))) else: factoids = get_factoid(event.session, name, None, pattern, is_regex, literal=True) if factoids: event.addresponse(u"I only know %(number)d things about %(name)s", { u'number': len(factoids), u'name': name, }) class Forget(Processor): usage = u"""forget [( # | //[r] )] is the same as """ feature = ('factoid',) priority = 10 permission = u'factoid' permissions = (u'factoidadmin',) @match(r'^forget\s+(.+?)(?:\s+#(\d+)|\s+(?:/(.+?)/(r?)))?$') @authorise(fallthrough=False) def forget(self, event, name, number, pattern, is_regex): factoids = get_factoid(event.session, name, number, pattern, is_regex, all=True, literal=True) if factoids: factoidadmin = auth_responses(event, u'factoidadmin') identities = get_identities(event) factoid = event.session.query(Factoid).get(factoids[0][0].id) if len(factoids) > 1 and pattern is not None: event.addresponse(u'Pattern matches multiple factoids, please be more specific') return if number or pattern: if factoids[0][2].identity_id not in identities and not factoidadmin: return if event.session.query(FactoidValue).filter_by(factoid_id=factoid.id).count() == 1: if len(filter(lambda x: x.identity_id not in identities, factoid.names)) > 0 and not factoidadmin: return id = factoid.id event.session.delete(factoid) event.session.commit() log.info(u"Deleted factoid %s (%s) by %s/%s (%s)", id, name, event.account, event.identity, event.sender['connection']) else: id = factoids[0][2].id event.session.delete(factoids[0][2]) event.session.commit() log.info(u"Deleted value %s (%s) of factoid %s (%s) by %s/%s (%s)", id, factoids[0][2].value, factoid.id, name, event.account, event.identity, event.sender['connection']) else: if factoids[0][1].identity_id not in identities and not factoidadmin: return if event.session.query(FactoidName).filter_by(factoid_id=factoid.id).count() == 1: if len(filter(lambda x: x.identity_id not in identities, factoid.values)) > 0 and not factoidadmin: return id = factoid.id event.session.delete(factoid) event.session.commit() log.info(u"Deleted factoid %s (%s) by %s/%s (%s)", id, name, event.account, event.identity, event.sender['connection']) else: id = factoids[0][1].id event.session.delete(factoids[0][1]) event.session.commit() log.info(u"Deleted name %s (%s) of factoid %s (%s) by %s/%s (%s)", id, factoids[0][1].name, factoid.id, factoids[0][0].names[0].name, event.account, event.identity, event.sender['connection']) event.addresponse(True) else: factoids = get_factoid(event.session, name, None, pattern, is_regex, all=True, literal=True) if factoids: event.addresponse(u"I only know %(number)d things about %(name)s", { u'number': len(factoids), u'name': name, }) else: event.addresponse(u"I didn't know about %s anyway", name) @match(r'^(.+)\s+is\s+the\s+same\s+as\s+(.+)$') @authorise(fallthrough=False) def alias(self, event, target, source): target = strip_name(target) if target == u'': event.addresponse(u"Sorry, I'm not interested in empty factoids") return if target.lower() == source.lower(): event.addresponse(u"That makes no sense, they *are* the same") return factoid = event.session.query(Factoid).join(Factoid.names) \ .filter(FactoidName.name==escape_name(source)).first() if factoid: target_factoid = event.session.query(FactoidName) \ .filter(FactoidName.name==escape_name(target)).first() if target_factoid: event.addresponse(u"I already know stuff about %s", target) return name = FactoidName(unicode(target), event.identity) factoid.names.append(name) event.session.save_or_update(factoid) event.session.commit() event.addresponse(True) log.info(u"Added name '%s' to factoid %s (%s) by %s/%s (%s)", name.name, factoid.id, factoid.names[0].name, event.account, event.identity, event.sender['connection']) else: event.addresponse(u"I don't know about %s", source) class Search(Processor): usage = u'search [for] [] [(facts|values) [containing]] (|//[r]) [from ]' feature = ('factoid',) limit = IntOption('search_limit', u'Maximum number of results to return', 30) default = IntOption('search_default', u'Default number of results to return', 10) regex_re = re.compile(r'^/(.*)/(r?)$') @match(r'^search\s+(?:for\s+)?(?:(\d+)\s+)?(?:(facts?|values?)\s+)?(?:containing\s+)?(.+?)(?:\s+from)?(?:\s+(\d+))?\s*$', version='deaddressed') def search(self, event, limit, search_type, pattern, start): limit = limit and min(int(limit), self.limit) or self.default start = start and max(int(start) - 1, 0) or 0 search_type = search_type and search_type.lower() or u"" origpattern = pattern m = self.regex_re.match(pattern) is_regex = False if m: pattern = m.group(1) is_regex = bool(m.group(2)) # Hack: We replace $arg with _%, but this won't match a partial # "$arg" string if is_regex: filter_op = get_regexp_op(event.session) name_pattern = pattern.replace(r'\$arg', '_%') else: filter_op = lambda x, y: x.like(y, escape='#') pattern = '%%%s%%' % escape_like_re.sub(r'#\1', pattern) name_pattern = pattern.replace('$arg', '#_#%') query = event.session.query(Factoid)\ .join(Factoid.names).add_entity(FactoidName)\ .join(Factoid.values) if search_type.startswith('fact'): query = query.filter(filter_op(FactoidName.name, name_pattern)) elif search_type.startswith('value'): query = query.filter(filter_op(FactoidValue.value, pattern)) else: query = query.filter(or_(filter_op(FactoidName.name, name_pattern), filter_op(FactoidValue.value, pattern))) # Pre-evalute the iterable or the if statement will be True in SQLAlchemy 0.4. LP: #383286 matches = [match for match in query.all()] bounded_matches = matches[start:start+limit] if bounded_matches: event.addresponse(u'; '.join( u'%s [%s]' % (fname.name, len(factoid.values)) for factoid, fname in bounded_matches)) elif len(matches): event.addresponse(u"I could only find %(number)d things that matched '%(pattern)s'", { u'number': len(matches), u'pattern': origpattern, }) else: event.addresponse(u"I couldn't find anything that matched '%s'" % origpattern) def _interpolate(message, event): "Expand factoid variables" utcnow = datetime.utcnow() now = utcnow.replace(tzinfo=tzutc()).astimezone(tzlocal()) message = message.replace(u'$who', event.sender['nick']) message = message.replace(u'$channel', event.channel) message = message.replace(u'$year', unicode(now.year)) message = message.replace(u'$month', unicode(now.month)) message = message.replace(u'$day', unicode(now.day)) message = message.replace(u'$hour', unicode(now.hour)) message = message.replace(u'$minute', unicode(now.minute)) message = message.replace(u'$second', unicode(now.second)) message = message.replace(u'$date', format_date(utcnow, 'date')) message = message.replace(u'$time', format_date(utcnow, 'time')) message = message.replace(u'$dow', unicode(now.strftime('%A'))) message = message.replace(u'$unixtime', unicode(utcnow.strftime('%s'))) return message class Get(Processor, RPC): usage = u' [( # | //[r] )]' feature = ('factoid',) priority = 200 interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives) verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs) def __init__(self, name): super(Get, self).__init__(name) RPC.__init__(self) def setup(self): self.get.im_func.pattern = re.compile( r'^(?:(?:%s)\s+(?:(?:%s)\s+)?)?(.+?)(?:\s+#(\d+))?(?:\s+/(.+?)/(r?))?$' % ('|'.join(self.interrogatives), '|'.join(self.verbs)), re.I) @handler def get(self, event, name, number, pattern, is_regex): response = self.remote_get(name, number, pattern, is_regex, event) if response: event.addresponse(response) def remote_get(self, name, number=None, pattern=None, is_regex=None, event={}): factoid = get_factoid(event.session, name, number, pattern, is_regex) if factoid: (factoid, fname, fvalue) = factoid reply = fvalue.value oname = fname.name pattern = re.escape(fname.name).replace(r'\$arg', '(.*)') args = re.match(pattern, name, re.I | re.U).groups() for i, capture in enumerate(args): reply = reply.replace('$%s' % (i + 1), capture) oname = oname.replace('$arg', capture, 1) reply = _interpolate(reply, event) (reply, count) = action_re.subn('', reply) if count: return {'action': True, 'reply': reply} (reply, count) = reply_re.subn('', reply) if count: return {'address': False, 'reply': reply} reply = u'%s %s' % (oname, reply) return reply class Set(Processor): usage = u""" (|==) [also] last set factoid""" feature = ('factoid',) interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives) verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs) priority = 800 permission = u'factoid' last_set_factoid = None def setup(self): self.set_factoid.im_func.pattern = re.compile( r'^(no[,.: ]\s*)?(.+?)\s+(also\s+)?(?:=(\S+)=)?(?(4)|(%s))(\s+also)?\s+((?(3).+|(?!.*=\S+=).+))$' % '|'.join(self.verbs), re.I) self.set_factoid.im_func.message_version = 'deaddressed' @handler @authorise(fallthrough=False) def set_factoid(self, event, correction, name, addition1, verb1, verb2, addition2, value): verb = verb1 or verb2 addition = addition1 or addition2 name = strip_name(name) if name == u'': event.addresponse(u"Sorry, I'm not interested in empty factoids") return if name.lower() in self.interrogatives: event.addresponse(choice(( u"I'm afraid I have no idea", u"Not a clue, sorry", u"Erk, dunno", ))) return factoid = event.session.query(Factoid).join(Factoid.names)\ .filter(FactoidName.name==escape_name(name)).first() if factoid: if correction: identities = get_identities(event) if not auth_responses(event, u'factoidadmin') and len(filter(lambda x: x.identity_id not in identities, factoid.values)) > 0: return for fvalue in factoid.values: event.session.delete(fvalue) elif not addition: event.addresponse(u'I already know stuff about %s', name) return else: factoid = Factoid() fname = FactoidName(unicode(name), event.identity) factoid.names.append(fname) event.session.save_or_update(factoid) event.session.flush() log.info(u"Creating factoid %s with name '%s' by %s", factoid.id, fname.name, event.identity) if not reply_re.match(value) and not action_re.match(value): value = '%s %s' % (verb, value) fvalue = FactoidValue(unicode(value), event.identity) factoid.values.append(fvalue) event.session.save_or_update(factoid) event.session.commit() self.last_set_factoid=factoid.names[0].name log.info(u"Added value '%s' to factoid %s (%s) by %s/%s (%s)", fvalue.value, factoid.id, factoid.names[0].name, event.account, event.identity, event.sender['connection']) event.addresponse(choice(( u'If you say so', u'One learns a new thing every day', u"I'll remember that", u'Got it', ))) @match(r'^(?:last\s+set\s+factoid|what\s+did\s+\S+\s+just\s+set)$') def last_set(self, event): if self.last_set_factoid is None: event.addresponse(u'Sorry, nobody has taught me anything recently') else: event.addresponse(u'It was: %s', self.last_set_factoid) class Modify(Processor): usage = u""" [( # | //[r] )] += [( # | //[r] )] ~= ( s///[g][i][r] | y/// )""" feature = ('factoid',) permission = u'factoid' permissions = (u'factoidadmin',) priority = 190 @match(r'^(.+?)(?:\s+#(\d+)|\s+/(.+?)/(r?))?\s*\+=(.+)$', version='deaddressed') @authorise(fallthrough=False) def append(self, event, name, number, pattern, is_regex, suffix): name = strip_name(name) factoids = get_factoid(event.session, name, number, pattern, is_regex, all=True, literal=True) if len(factoids) == 0: if pattern: event.addresponse(u"I don't know about any %(name)s matching %(pattern)s", { 'name': name, 'pattern': pattern, }) else: factoids = get_factoid(event.session, name, None, pattern, is_regex, all=True) if factoids: event.addresponse(u"I only know %(number)d things about %(name)s", { u'number': len(factoids), u'name': name, }) else: event.addresponse(u"I don't know about %s", name) elif len(factoids) > 1 and number is None: event.addresponse(u"Pattern matches multiple factoids, please be more specific") else: factoidadmin = auth_responses(event, u'factoidadmin') identities = get_identities(event) factoid = factoids[0] if factoid[2].identity_id not in identities and not factoidadmin: return oldvalue = factoid[2].value factoid[2].value += suffix event.session.save_or_update(factoid[2]) event.session.commit() log.info(u"Appended '%s' to value %s of factoid %s (%s) by %s/%s (%s)", suffix, factoid[2].id, factoid[0].id, oldvalue, event.account, event.identity, event.sender['connection']) event.addresponse(True) @match(r'^(.+?)(?:\s+#(\d+)|\s+/(.+?)/(r?))?\s*(?:~=|=~)\s*([sy](?P.).+(?P=sep).*(?P=sep)[gir]*)$') @authorise(fallthrough=False) def modify(self, event, name, number, pattern, is_regex, operation, separator): factoids = get_factoid(event.session, name, number, pattern, is_regex, all=True) if len(factoids) == 0: if pattern: event.addresponse(u"I don't know about any %(name)s matching %(pattern)s", { 'name': name, 'pattern': pattern, }) else: event.addresponse(u"I don't know about %s", name) elif len(factoids) > 1: event.addresponse(u"Pattern matches multiple factoids, please be more specific") else: factoidadmin = auth_responses(event, u'factoidadmin') identities = get_identities(event) factoid = factoids[0] if factoid[2].identity_id not in identities and not factoidadmin: return # Not very pythonistic, but escaping is a nightmare. parts = [[]] pos = 0 while pos < len(operation): char = operation[pos] if char == separator: parts.append([]) elif char == u"\\": if pos < len(operation) - 1: if operation[pos+1] == u"\\": parts[-1].append(u"\\") pos += 1 elif operation[pos+1] == separator: parts[-1].append(separator) pos += 1 else: parts[-1].append(u"\\") else: parts[-1].append(u"\\") else: parts[-1].append(char) pos += 1 parts = [u"".join(x) for x in parts] if len(parts) != 4: event.addresponse(u"That operation makes no sense. Try something like s/foo/bar/") return oldvalue = factoid[2].value op, search, replace, flags = parts flags = flags.lower() if op == "s": if "r" in flags: if "i" in flags: search += "(?i)" try: factoid[2].value = re.sub(search, replace, oldvalue, int("g" not in flags)) except: event.addresponse(u"That operation makes no sense. Try something like s/foo/bar/") return else: newvalue = oldvalue.replace(search, replace, "g" in flags and -1 or 1) if newvalue == oldvalue: event.addresponse(u"I couldn't find '%(terms)s' in '%(oldvalue)s'. If that was a proper regular expression, append the 'r' flag", { 'terms': search, 'oldvalue': oldvalue, }) return factoid[2].value = newvalue elif op == "y": if len(search) != len(replace): event.addresponse(u"That operation makes no sense. The source and destination must be the same length") return try: table = dict((ord(x), ord(y)) for x, y in zip(search, replace)) factoid[2].value = oldvalue.translate(table) except: event.addresponse(u"That operation makes no sense. Try something like y/abcdef/ABCDEF/") return event.session.save_or_update(factoid[2]) event.session.commit() log.info(u"Applying '%s' to value %s of factoid %s (%s) by %s/%s (%s)", operation, factoid[2].id, factoid[0].id, oldvalue, event.account, event.identity, event.sender['connection']) event.addresponse(True) greetings = ( u'lo', u'ello', u'hello', u'hi', u'hi there', u'howdy', u'hey', u'heya', u'hiya', u'hola', u'salut', u'bonjour', u'sup', u'wussup', u'hoezit', u'wotcha', u'wotcher', u'yo', u'word', u'good day', u'wasup', u'wassup', u'howzit', u'howsit', u'buon giorno', u'hoe lyk it', u'hoe gaan dit', u'good morning', u'morning', u'afternoon', u'evening', ) static_default = { 'greet': { 'matches': [r'\b(' + '|'.join(list(greetings) + [g.replace(' ', '') for g in greetings if ' ' in g]) + r')\b'], 'responses': greetings, }, 'reward': { 'matches': [r'\bbot(\s+|\-)?snack\b'], 'responses': [u'thanks, $who', u'$who: thankyou!', u':)'], }, 'praise': { 'matches': [r'\bgood(\s+fuckin[\'g]?)?\s+(lad|bo(t|y)|g([ui]|r+)rl)\b', r'\byou\s+(rock|rocks|rewl|rule|are\s+so+\s+co+l)\b'], 'responses': [u'thanks, $who', u'$who: thankyou!', u':)'], }, 'thanks': { 'matches': [r'\bthank(s|\s*you)\b', r'^\s*ta\s*$', r'^\s*shot\s*$'], 'responses': [u'no problem, $who', u'$who: my pleasure', u'sure thing, $who', u'no worries, $who', u'$who: np', u'no probs, $who', u'$who: no problemo', u'$who: not at all'], }, 'criticism': { 'matches': [r'\b((kak|bad|st(u|oo)pid|dumb)(\s+fuckin[\'g]?)?\s+(bo(t|y)|g([ui]|r+)rl))|(bot(\s|\-)?s(mack|lap))\b'], 'responses': [u'*whimper*', u'sorry, $who :(', u':(', u'*cringe*'], }, } class StaticFactoid(Processor): priority = 900 extras = Option('static', 'List of static factoids using regexes', {}) def setup(self): self.factoids = static_default.copy() self.factoids.update(self.extras) @handler def static(self, event): for factoid in self.factoids.values(): for match in factoid['matches']: if re.search(match, event.message['stripped'], re.I|re.DOTALL): event.addresponse(_interpolate(choice(factoid['responses']), event), address=False) return # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/eval.py0000644000000000000000000000306511414117331015007 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. try: import perl except ImportError: pass try: import lua except ImportError: pass from ibid.plugins import Processor, match, authorise features = {'eval': { 'description': u'Evaluates Python, Perl and Lua code.', 'categories': ('debug',), }} class Python(Processor): usage = u'py ' feature = ('eval',) permission = u'eval' @match(r'^py(?:thon)?\s+(.+)$') @authorise() def eval(self, event, code): try: globals = {} exec('import os', globals) exec('import sys', globals) exec('import re', globals) exec('import time', globals) result = eval(code, globals, {}) except Exception, e: result = e event.addresponse(repr(result)) class Perl(Processor): usage = u'pl ' feature = ('eval',) permission = u'eval' @match(r'^(?:perl|pl)\s+(.+)$') @authorise() def eval(self, event, code): try: result = perl.eval(code) except Exception, e: result = e event.addresponse(repr(result)) class Lua(Processor): usage = u'lua ' feature = ('eval',) permission = u'eval' @match(r'^lua\s+(.+)$') @authorise() def eval(self, event, code): try: result = lua.eval(code) except Exception, e: result = e event.addresponse(repr(result)) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/quotes.py0000644000000000000000000003457411531040264015411 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera, Max Rabkin # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from urllib2 import urlopen, HTTPError from urllib import urlencode, quote from httplib import BadStatusLine from urlparse import urljoin from random import choice, shuffle, randint from sys import exc_info from subprocess import Popen, PIPE import logging import re from ibid.compat import ElementTree from ibid.config import Option, BoolOption from ibid.plugins import Processor, match, RPC from ibid.utils.html import get_html_parse_tree from ibid.utils import file_in_path, unicode_output log = logging.getLogger('plugins.quotes') features = {} features['fortune'] = { 'description': u'Returns a random fortune.', 'categories': ('fun', 'lookup',), } class Fortune(Processor, RPC): usage = u'fortune' feature = ('fortune',) fortune = Option('fortune', 'Path of the fortune executable', 'fortune') def __init__(self, name): super(Fortune, self).__init__(name) RPC.__init__(self) def setup(self): if not file_in_path(self.fortune): raise Exception("Cannot locate fortune executable") @match(r'^fortune$') def handler(self, event): fortune = self.remote_fortune() if fortune: event.addresponse(fortune) else: event.addresponse(u"Couldn't execute fortune") def remote_fortune(self): fortune = Popen(self.fortune, stdout=PIPE, stderr=PIPE) output, error = fortune.communicate() code = fortune.wait() output = unicode_output(output.strip(), 'replace') if code == 0: return output else: return None features['bash'] = { 'description': u'Retrieve quotes from bash.org.', 'categories': ('fun', 'lookup', 'web',), } class Bash(Processor): usage = u'bash[.org] [(random|)]' feature = ('bash',) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) @match(r'^bash(?:\.org)?(?:\s+(random|\d+))?$') def bash(self, event, id): id = id is None and u'random' or id.lower() if id == u'random' and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return soup = get_html_parse_tree('http://bash.org/?%s' % id) number = u"".join(soup.find('p', 'quote').find('b').contents) output = [u'%s:' % number] body = soup.find('p', 'qt') if not body: event.addresponse(u"There's no such quote, but if you keep talking like that maybe there will be") else: for line in body.contents: line = unicode(line).strip() if line != u'
': output.append(line) event.addresponse(u'\n'.join(output), conflate=False) features['fml'] = { 'description': u'Retrieves quotes from fmylife.com.', 'categories': ('fun', 'lookup', 'web',), } class FMLException(Exception): pass class FMyLife(Processor): usage = u'fml ( | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )' feature = ('fml',) api_url = Option('fml_api_url', 'FML API URL base', 'http://api.betacie.com/') # The Ibid API Key, registered by Stefano Rivera: api_key = Option('fml_api_key', 'FML API Key', '4b39a7fcaf01c') fml_lang = Option('fml_lang', 'FML Lanugage', 'en') public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) failure_messages = ( u'Today, I tried to get a quote for %(nick)s but failed. FML', u'Today, FML is down. FML', u"Sorry, it's broken, the FML admins must having a really bad day", ) def remote_get(self, id): url = urljoin(self.api_url, 'view/%s?%s' % ( id.isalnum() and id + '/nocomment' or quote(id), urlencode({'language': self.fml_lang, 'key': self.api_key})) ) f = urlopen(url) try: tree = ElementTree.parse(f) except SyntaxError: class_, e, tb = exc_info() new_exc = FMLException(u'XML Parsing Error: %s' % unicode(e)) raise new_exc.__class__, new_exc, tb if tree.find('.//error'): raise FMLException(tree.findtext('.//error')) item = tree.find('.//item') if item: url = u"http://www.fmylife.com/%s/%s" % ( item.findtext('category'), item.get('id'), ) text = item.find('text').text return u'%s\n- %s' % (text, url) @match(r'^(?:fml\s+|http://www\.fmylife\.com/\S+/)(\d+|random|flop|top|last|love|money|kids|work|health|sex|miscellaneous)$') def fml(self, event, id): try: body = self.remote_get(id) except (FMLException, HTTPError, BadStatusLine): event.addresponse(choice(self.failure_messages) % event.sender) return if body: event.addresponse(body) elif id.isdigit(): event.addresponse(u'No such quote') else: event.addresponse(choice(self.failure_messages) % event.sender) @match(r'^fml$') def fml_default(self, event): if not event.public or self.public_browse: self.fml(event, 'random') else: event.addresponse(u'Sorry, not in public. PM me') features['tfln'] = { 'description': u'Looks up quotes from textsfromlastnight.com', 'categories': ('fun', 'lookup', 'web',), } class TextsFromLastNight(Processor): usage = u"""tfln [(random|)] tfln (worst|best) [(today|this week|this month)]""" feature = ('tfln',) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) random_pool = [] def get_tfln(self, section): tree = get_html_parse_tree('http://textsfromlastnight.com/%s' % section, treetype='etree') ul = [x for x in tree.findall('.//ul') if x.get('id') == 'texts-list'][0] id_re = re.compile('^/Text-Replies-(\d+)\.html$') for li in ul.findall('li'): id = 0 message='' div = [x for x in li.findall('div') if x.get('class') == 'text'][0] for a in div.findall('.//a'): href = a.get('href') if href.startswith('/Texts-From-Areacode-'): message += u'\n' + a.text elif href.startswith('/Text-Replies-'): id = int(id_re.match(href).group(1)) message += a.text yield id, message.strip() @match(r'^tfln' r'(?:\s+(random|worst|best|\d+))?' r'(?:this\s+)?(?:\s+(today|week|month))?$') def tfln(self, event, number, timeframe=None): number = number is None and u'random' or number.lower() if number == u'random' and not timeframe \ and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return if number in (u'worst', u'best'): number = u'Texts-From-%s-Nights' % number.title() if timeframe: number += u'-' + timeframe.title() number += u'.html' elif number.isdigit(): number = 'Text-Replies-%s.html' % number if number == u'random': if not self.random_pool: self.random_pool = [message for message in self.get_tfln(u'Random-Texts-From-Last-Night.html')] shuffle(self.random_pool) message = self.random_pool.pop() else: try: message = self.get_tfln(number).next() except StopIteration: event.addresponse(u'No such quote') return id, body = message event.addresponse( u'%(body)s\n' u'- http://textsfromlastnight.com/Text-Replies-%(id)i.html', { 'id': id, 'body': body, }, conflate=False) @match(r'^(?:http://)?(?:www\.)?textsfromlastnight\.com/' r'Text-Replies-(\d+).html$') def tfln_url(self, event, id): self.tfln(event, id) features['mlia'] = { 'description': u'Looks up quotes from MyLifeIsAverage.com and MyLifeIsG.com', 'categories': ('fun', 'lookup', 'web',), } class MyLifeIsAverage(Processor): usage = (u"mlia [( | random | recent | today | yesterday | " u"this week | this month | this year )]") feature = ('mlia',) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) random_pool = [] pages = 1 def find_stories(self, url): if isinstance(url, basestring): tree = get_html_parse_tree(url, treetype='etree') else: tree = url stories = [div for div in tree.findall('.//div') if div.get(u'class') == u'story s'] for story in stories: body = story.findtext('div').strip() id = story.findtext('.//a') if isinstance(id, basestring) and id[1:].isdigit(): id = int(id[1:]) yield id, body @match(r'^mlia(?:\s+this)?' r'(?:\s+(\d+|random|recent|today|yesterday|week|month|year))?$') def mlia(self, event, query): query = query is None and u'random' or query.lower() if query == u'random' and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return url = 'http://mylifeisaverage.com/' if query == u'random' or query is None: if not self.random_pool: purl = url + str(randint(1, self.pages)) tree = get_html_parse_tree(purl, treetype='etree') self.random_pool = list(self.find_stories(tree)) shuffle(self.random_pool) pagination = [ul for ul in tree.findall('.//ul') if ul.get(u'class') == u'pages'][0] self.pages = int( [li for li in pagination.findall('li') if li.get(u'class') == u'last'][0] .find(u'a').get(u'href')) story = self.random_pool.pop() else: try: if query.isdigit(): surl = url + '/s/' + query else: surl = url + '/best/' + query story = self.find_stories(surl).next() except StopIteration: event.addresponse(u'No such quote') return id, body = story url += 's/%i' % id event.addresponse(u'%(body)s\n- %(url)s', { 'url': url, 'body': body, }) @match(r'^(?:http://)?(?:www\.)?mylifeisaverage\.com' r'/s/(\d+)$') def mlia_url(self, event, id): self.mlia(event, id) features['bible'] = { 'description': u'Retrieves Bible verses', 'categories': ('lookup', 'web',), } class Bible(Processor): usage = u"""bible [in ] [in ]""" feature = ('bible',) # http://labs.bible.org/api/ is an alternative # Their feature set is a little different, but they should be fairly # compatible api_url = Option('bible_api_url', 'Bible API URL base', 'http://api.preachingcentral.com/bible.php') psalm_pat = re.compile(r'\bpsalm\b', re.IGNORECASE) # The API doesn't seem to work with the apocrypha, even when looking in # versions that include it books = '|'.join(['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth', '(?:1|2|I|II) Samuel', '(?:1|2|I|II) Kings', '(?:1|2|I|II) Chronicles', 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms?', 'Proverbs', 'Ecclesiastes', 'Song(?: of (?:Songs|Solomon)?)?', 'Canticles', 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi', 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '(?:1|2|I|II) Corinthians', 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '(?:1|2|I|II) Thessalonians', '(?:1|2|I|II) Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '(?:1|2|I|II) Peter', '(?:1|2|3|I|II|III) John', 'Jude', 'Revelations?(?: of (?:St.|Saint) John)?']).replace(' ', '\s*') @match(r'^bible\s+(.*?)(?:\s+(?:in|from)\s+(.*))?$') def bible(self, event, passage, version=None): passage = self.psalm_pat.sub('psalms', passage) params = {'passage': passage.encode('utf-8'), 'type': 'xml', 'formatting': 'plain'} if version: params['version'] = version.lower().encode('utf-8') f = urlopen(self.api_url + '?' + urlencode(params)) tree = ElementTree.parse(f) message = self.formatPassage(tree) if message: event.addresponse(message) errors = list(tree.findall('.//error')) if errors: event.addresponse(u'There were errors: %s.', '. '.join(err.text for err in errors)) elif not message: event.addresponse(u"I couldn't find that passage.") # Allow queries which are quite definitely bible references to omit "bible". # Specifically, they must start with the name of a book and be followed only # by book names, chapters and verses. @match(r'^((?:(?:' + books + ')(?:\d|[-:,]|\s)*)+?)(?:\s+(?:in|from)\s+(.*))?$') def bookbible(self, *args): self.bible(*args) def formatPassage(self, xml): message = [] oldref = (None, None, None) for item in xml.findall('.//item'): ref, text = self.verseInfo(item) if oldref[0] != ref[0]: message.append(u'(%s %s:%s)' % ref) elif oldref[1] != ref[1]: message.append(u'(%s:%s)' % ref[1:]) else: message.append(u'%s' % ref[2]) oldref = ref message.append(text) return u' '.join(message) def verseInfo(self, xml): book, chapter, verse, text = map(xml.findtext, ('bookname', 'chapter', 'verse', 'text')) return ((book, chapter, verse), text) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/oeis.py0000644000000000000000000000524611515632641015032 0ustar rootroot# Copyright (c) 2009-2010, Max Rabkin # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from urllib2 import urlopen import re import logging from ibid.compat import defaultdict from ibid.plugins import Processor, match from ibid.utils import plural log = logging.getLogger('plugins.oeis') features = {'oeis': { 'description': 'Query the Online Encyclopedia of Integer Sequences', 'categories': ('lookup', 'web', 'calculate',), }} class OEIS(Processor): usage = u"""oeis (A|M|N) oeis [, ...]""" feature = ('oeis',) @match(r'^oeis\s+([AMN]\d+|-?\d(?:\d|-|,|\s)*)$') def oeis (self, event, query): query = re.sub(r'(,|\s)+', ',', query) f = urlopen('http://oeis.org/search?n=1&fmt=text&q=' + query) for i in range(3): f.next() # the first lines are uninteresting results_m = re.search(r'Showing .* of (\d+)', f.next()) if results_m: f.next() sequence = Sequence(f) event.addresponse(u'%(name)s - %(url)s - %(values)s', {'name': sequence.name, 'url': sequence.url(), 'values': sequence.values}) results = int(results_m.group(1)) if results > 1: event.addresponse(u'There %(was)s %(count)d more %(results)s. ' u'See %(url)s%(query)s for more.', {'was': plural(results-1, 'was', 'were'), 'count': results-1, 'results': plural(results-1, 'result', 'results'), 'url': 'http://oeis.org/search?q=', 'query': query}) else: event.addresponse(u"I couldn't find that sequence.") class Sequence(object): def __init__ (self, lines): cmds = defaultdict(list) for line in lines: line = line.lstrip()[:-1] if not line: break line_m = re.match(r'%([A-Z]) (A\d+)(?: (.*))?$', line) if line_m: cmd, self.catalog_num, info = line_m.groups() cmds[cmd].append(info) else: cmds[cmd][-1] += line # %V, %W and %X give signed values if the sequence is signed. # Otherwise, only %S, %T and %U are given. self.values = (''.join(cmds['V'] + cmds['W'] + cmds['X']) or ''.join(cmds['S'] + cmds['T'] + cmds['U'])) self.name = ''.join(cmds['N']) def url (self): return 'http://oeis.org/' + self.catalog_num # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/knab.py0000644000000000000000000000310411362435477015005 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import perl from ibid.plugins import Processor, handler class Knab(Processor): autoload = False knabdir = '../knab/' config = 'Knab.cfg' priority = 500 def __init__(self, name): Processor.__init__(self, name) perl.eval('use lib "%s"' % self.knabdir) perl.require('Knab::Dumper') perl.require('Knab::Conf') perl.require('Knab::Modules') perl.require('Knab::Processor') perl.eval('$::dumper=new Knab::Dumper();') perl.eval('$::config = new Knab::Conf(Basedir=>"%s", Filename=>"%s");' % (self.knabdir, self.config)) factoidDB = perl.eval('$::config->getValue("FactoidDB/module");') perl.require(factoidDB) perl.eval('$::db=new %s();' % factoidDB) modules = perl.callm('new', 'Knab::Modules') self.processor = perl.callm('new', 'Knab::Processor', modules) @handler def handler(self, event): event.input = event.message['clean'] event.oldinput = event.message['raw'] event.withpunc = event.message['raw'] self.processor.Process(event) responses = [] for response in event.responses: if 'items' in dir(response): new = {} for key, value in response.items(): new[key] = value responses.append(new) else: responses.append(response) event.responses = responses # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/sysadmin.py0000644000000000000000000001711611414117331015711 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import os import re from subprocess import Popen, PIPE from ibid.plugins import Processor, match from ibid.config import Option from ibid.utils import file_in_path, unicode_output, human_join, cacheable_download features = {} features['aptitude'] = { 'description': u'Searches for packages', 'categories': ('sysadmin', 'lookup',), } class Aptitude(Processor): usage = u'apt (search|show) ' feature = ('aptitude',) aptitude = Option('aptitude', 'Path to aptitude executable', 'aptitude') bad_search_strings = ( "?action", "~a", "?automatic", "~A", "?broken", "~b", "?config-files", "~c", "?garbage", "~g", "?installed", "~i", "?new", "~N", "?obsolete", "~o", "?upgradable", "~U", "?user-tag", "?version", "~V" ) def setup(self): if not file_in_path(self.aptitude): raise Exception("Cannot locate aptitude executable") def _check_terms(self, event, term): "Check for naughty users" for word in self.bad_search_strings: if word in term: event.addresponse(u"I can't tell you about my host system. Sorry") return False if term.strip().startswith("-"): event.addresponse(False) return False return True @match(r'^(?:apt|aptitude|apt-get|apt-cache)\s+search\s+(.+)$') def search(self, event, term): if not self._check_terms(event, term): return apt = Popen([self.aptitude, 'search', '-F', '%p', term], stdout=PIPE, stderr=PIPE) output, error = apt.communicate() code = apt.wait() if code == 0: if output: output = unicode_output(output.strip()) output = [line.strip() for line in output.splitlines()] event.addresponse(u'Found %(num)i packages: %(names)s', { 'num': len(output), 'names': human_join(output), }) else: event.addresponse(u'No packages found') else: error = unicode_output(error.strip()) if error.startswith(u"E: "): error = error[3:] event.addresponse(u"Couldn't search: %s", error) @match(r'^(?:apt|aptitude|apt-get)\s+show\s+(.+)$') def show(self, event, term): if not self._check_terms(event, term): return apt = Popen([self.aptitude, 'show', term], stdout=PIPE, stderr=PIPE) output, error = apt.communicate() code = apt.wait() if code == 0: description = None provided = None output = unicode_output(output) real = True for line in output.splitlines(): if not description: if line.startswith(u'Description:'): description = u'%s:' % line.split(None, 1)[1] elif line.startswith(u'Provided by:'): provided = line.split(None, 2)[2] elif line == u'State: not a real package': real = False elif line != "": description += u' ' + line.strip() else: # More than one package listed break if provided: event.addresponse(u'Virtual package provided by %s', provided) elif description: event.addresponse(description) elif not real: event.addresponse(u'Virtual package, not provided by anything') else: raise Exception("We couldn't successfully parse aptitude's output") else: error = unicode_output(error.strip()) if error.startswith(u"E: "): error = error[3:] event.addresponse(u"Couldn't find package: %s", error) features['apt-file'] = { 'description': u'Searches for packages containing the specified file', 'categories': ('sysadmin', 'lookup',), } class AptFile(Processor): usage = u'apt-file [search] ' feature = ('apt-file',) aptfile = Option('apt-file', 'Path to apt-file executable', 'apt-file') def setup(self): if not file_in_path(self.aptfile): raise Exception("Cannot locate apt-file executable") @match(r'^apt-?file\s+(?:search\s+)?(.+)$') def search(self, event, term): apt = Popen([self.aptfile, 'search', term], stdout=PIPE, stderr=PIPE) output, error = apt.communicate() code = apt.wait() if code == 0: if output: output = unicode_output(output.strip()) output = [line.split(u':')[0] for line in output.splitlines()] packages = sorted(set(output)) event.addresponse(u'Found %(num)i packages: %(names)s', { 'num': len(packages), 'names': human_join(packages), }) else: event.addresponse(u'No packages found') else: error = unicode_output(error.strip()) if u"The cache directory is empty." in error: event.addresponse(u'Search error: apt-file cache empty') else: event.addresponse(u'Search error') raise Exception("apt-file: %s" % error) features['man'] = { 'description': u'Retrieves information from manpages.', 'categories': ('sysadmin', 'lookup',), } class Man(Processor): usage = u'man [
] ' feature = ('man',) man = Option('man', 'Path of the man executable', 'man') def setup(self): if not file_in_path(self.man): raise Exception("Cannot locate man executable") @match(r'^man\s+(?:(\d)\s+)?(\S+)$') def handle_man(self, event, section, page): command = [self.man, page] if section: command.insert(1, section) if page.strip().startswith("-"): event.addresponse(False) return env = os.environ.copy() env["COLUMNS"] = "500" man = Popen(command, stdout=PIPE, stderr=PIPE, env=env) output, error = man.communicate() code = man.wait() if code != 0: event.addresponse(u'Manpage not found') else: output = unicode_output(output.strip(), errors="replace") output = output.splitlines() index = output.index('NAME') if index: event.addresponse(output[index+1].strip()) index = output.index('SYNOPSIS') if index: event.addresponse(output[index+1].strip()) features['mac'] = { 'description': u'Finds the organization owning the specific MAC address.', 'categories': ('sysadmin', 'lookup',), } class Mac(Processor): usage = u'mac
' feature = ('mac',) @match(r'^((?:mac|oui|ether(?:net)?(?:\s*code)?)\s+)?((?:(?:[0-9a-f]{2}(?(1)[:-]?|:))){2,5}[0-9a-f]{2})$') def lookup_mac(self, event, _, mac): oui = mac.replace('-', '').replace(':', '').upper()[:6] ouis = open(cacheable_download('http://standards.ieee.org/regauth/oui/oui.txt', 'sysadmin/oui.txt')) match = re.search(r'^%s\s+\(base 16\)\s+(.+?)$' % oui, ouis.read(), re.MULTILINE) ouis.close() if match: name = match.group(1).decode('utf8').title() event.addresponse(u"That belongs to %s", name) else: event.addresponse(u"I don't know who that belongs to") # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/urlgrab.py0000644000000000000000000001366611530271355015534 0ustar rootroot# Copyright (c) 2009-2011, Michael Gorven, Stefano Rivera, Jonathan Groll # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import datetime from httplib import BadStatusLine from urllib import urlencode from urllib2 import build_opener, HTTPError, HTTPBasicAuthHandler import logging import re import ibid from ibid.plugins import Processor, handler from ibid.config import Option from ibid.db import IbidUnicode, IbidUnicodeText, Integer, DateTime, \ Table, Column, ForeignKey, Base, VersionedSchema from ibid.utils import url_regex from ibid.utils.html import get_html_parse_tree log = logging.getLogger('plugins.urlgrab') features = {'urlgrab': { 'description': u'Captures URLs seen in channel to database and/or to ' u'delicious/faves', 'categories': ('remember', 'web',), }} class URL(Base): __table__ = Table('urls', Base.metadata, Column('id', Integer, primary_key=True), Column('url', IbidUnicodeText, nullable=False), Column('channel', IbidUnicode(32, case_insensitive=True), nullable=False), Column('identity_id', Integer, ForeignKey('identities.id'), nullable=False, index=True), Column('time', DateTime, nullable=False), useexisting=True) class URLSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.identity_id) def upgrade_2_to_3(self): self.alter_column(Column('url', IbidUnicodeText, nullable=False)) self.alter_column(Column('channel', IbidUnicode(32, case_insensitive=True), nullable=False), force_rebuild=True) __table__.versioned_schema = URLSchema(__table__, 3) def __init__(self, url, channel, identity_id): self.url = url self.channel = channel self.identity_id = identity_id self.time = datetime.utcnow() class Grab(Processor): addressed = False processed = True username = Option('username', 'Account name for URL posting') password = Option('password', 'Password for URL Posting') service = Option('service', 'URL Posting Service (delicious/faves)', 'delicious') def setup(self): self.grab.im_func.pattern = re.compile(( r'(?:[^@./]\b(?!\.)|\A)(' # Match a boundary, but not on an e-mail address + url_regex() + r')[\[>)\]"\'.,;:]*(?:\s|\Z)' # End boundary ), re.I | re.DOTALL) @handler def grab(self, event, url): if url.find('://') == -1: if url.lower().startswith('ftp'): url = 'ftp://%s' % url else: url = 'http://%s' % url u = URL(url, event.channel, event.identity) event.session.save_or_update(u) if self.service and self.username: self._post_url(event, url) def _post_url(self, event, url=None): "Posts a URL to delicious.com" title = self._get_title(url) con_re = re.compile(r'!n=|!') connection_body = con_re.split(event.sender['connection']) if len(connection_body) == 1: connection_body.append(event.sender['connection']) ip_re = re.compile(r'\.IP$|unaffiliated') if ip_re.search(connection_body[1]) != None: connection_body[1] = '' if ibid.sources[event.source].type == 'jabber': obfusc_conn = '' obfusc_chan = event.channel.replace('@', '^') else: at_re = re.compile(r'@\S+?\.') obfusc_conn = at_re.sub('^', connection_body[1]) obfusc_chan = at_re.sub('^', event.channel) tags = u' '.join((event.sender['nick'], obfusc_conn, obfusc_chan, event.source)) data = { 'url' : url.encode('utf-8'), 'description' : title.encode('utf-8'), 'tags' : tags.encode('utf-8'), 'replace' : 'yes', 'dt' : event.time.strftime('%Y-%m-%dT%H:%M:%SZ'), 'extended' : event.message['raw'].encode('utf-8'), } if self.service.lower() == 'delicious': service = ('del.icio.us API', 'https://api.del.icio.us') elif self.service.lower() == 'faves': service = ('Faves', 'https://secure.faves.com') else: log.error(u'Unknown social bookmarking service: %s', self.service) return auth_handler = HTTPBasicAuthHandler() auth_handler.add_password(service[0], service[1], self.username, self.password) opener = build_opener(auth_handler) posturl = service[1] + '/v1/posts/add?' + urlencode(data) try: resp = opener.open(posturl).read() if 'done' in resp: log.debug(u"Posted url '%s' to %s, posted in %s on %s " u"by %s/%i (%s)", url, self.service, event.channel, event.source, event.account, event.identity, event.sender['connection']) else: log.error(u"Error posting url '%s' to %s: %s", url, self.service, resp) except HTTPError, e: if e.code == 401: log.error(u"Incorrect password for %s, couldn't post", self.service) except BadStatusLine, e: log.error(u"Error posting url '%s' to %s: %s", url, self.service, unicode(e)) def _get_title(self, url): "Gets the title of a page" try: headers = {'User-Agent': 'Mozilla/5.0'} etree = get_html_parse_tree(url, None, headers, 'etree') title = etree.findtext('head/title') return title or url except Exception, e: log.debug(u"Error determining title for %s: %s", url, unicode(e)) return url # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/urlinfo.py0000644000000000000000000001013211414117331015527 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. # # The youtube Processor is inspired by (and steals the odd RE from) youtube-dl: # Copyright (c) 2006-2008 Ricardo Garcia Gonzalez # Released under MIT Licence from cgi import parse_qs from urllib import urlencode from urllib2 import urlopen, build_opener, HTTPError, HTTPRedirectHandler import logging import re from ibid.plugins import Processor, handler, match from ibid.config import ListOption default_user_agent = 'Mozilla/5.0' default_referer = "http://ibid.omnia.za.net/" features = {} log = logging.getLogger('plugins.url') features['tinyurl'] = { 'description': u'Shorten and lengthen URLs', 'categories': ('lookup', 'web',), } class Shorten(Processor): usage = u'shorten ' feature = ('tinyurl',) @match(r'^shorten\s+(\S+\.\S+)$') def shorten(self, event, url): f = urlopen('http://is.gd/api.php?%s' % urlencode({'longurl': url})) shortened = f.read() f.close() event.addresponse(u'That reduces to: %s', shortened) class NullRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, hdrs, newurl): return None class Lengthen(Processor): usage = u""" expand """ feature = ('tinyurl',) services = ListOption('services', 'List of URL prefixes of URL shortening services', ( 'http://is.gd/', 'http://tinyurl.com/', 'http://ff.im/', 'http://shorl.com/', 'http://icanhaz.com/', 'http://url.omnia.za.net/', 'http://snipurl.com/', 'http://tr.im/', 'http://snipr.com/', 'http://bit.ly/', 'http://cli.gs/', 'http://zi.ma/', 'http://twurl.nl/', 'http://xrl.us/', 'http://lnk.in/', 'http://url.ie/', 'http://ne1.net/', 'http://turo.us/', 'http://301url.com/', 'http://u.nu/', 'http://twi.la/', 'http://ow.ly/', 'http://su.pr/', 'http://tiny.cc/', 'http://ur1.ca/', )) def setup(self): self.lengthen.im_func.pattern = re.compile(r'^(?:((?:%s)\S+)|(?:lengthen\s+|expand\s+)(http://\S+))$' % '|'.join([re.escape(service) for service in self.services]), re.I|re.DOTALL) @handler def lengthen(self, event, url1, url2): url = url1 or url2 opener = build_opener(NullRedirect()) try: f = opener.open(url) f.close() except HTTPError, e: if e.code in (301, 302, 303, 307): event.addresponse(u'That expands to: %s', e.hdrs['location']) return raise event.addresponse(u"No redirect") features['youtube'] = { 'description': u'Determine the title and a download URL for a Youtube Video', 'categories': ('lookup', 'web',), } class Youtube(Processor): usage = u'' feature = ('youtube',) @match(r'^(?:youtube(?:\.com)?\s+)?' r'(?:http://)?(?:\w+\.)?youtube\.com/' r'(?:v/|(?:watch(?:\.php)?)?\?(?:.+&)?v=)' r'([0-9A-Za-z_-]+)(?(1)[&/].*)?$') def youtube(self, event, id): for el_type in ('embedded', 'detailpage', 'vevo'): url = 'http://www.youtube.com/get_video_info?' + urlencode({ 'video_id': id, 'el': el_type, 'ps': 'default', 'eurl': '', 'gl': 'US', 'hl': 'en', }) info = parse_qs(urlopen(url).read()) if info.get('status', [None])[0] == 'ok': break if info.get('status', [None])[0] == 'ok': event.addresponse(u'%(title)s: %(url)s', { 'title': info['title'][0].decode('utf-8'), 'url': 'http://www.youtube.com/get_video?' + urlencode({ 'video_id': id, 't': info['token'][0], }), }) else: event.addresponse(u"Sorry, I couldn't retreive that, YouTube says: " u"%(status)s: %(reason)s", { 'status': info['status'][0], 'reason': info['reason'][0], }) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/trac.py0000644000000000000000000001141111414117331015003 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import datetime import logging from sqlalchemy import Table, MetaData from sqlalchemy.orm import mapper from sqlalchemy.sql import func import ibid from ibid.plugins import Processor, match, RPC from ibid.config import Option, BoolOption from ibid.utils import ago features = {'trac': { 'description': u'Retrieves tickets from a Trac database.', 'categories': ('development', 'lookup',), }} class Ticket(object): pass if 'trac' in ibid.databases: session = ibid.databases.trac() metadata = MetaData(bind=ibid.databases.trac().bind) ticket_table = Table('ticket', metadata, autoload=True) mapper(Ticket, ticket_table) class Tickets(Processor, RPC): usage = u"""ticket (open|my|'s) tickets""" feature = ('trac',) autoload = 'trac' in ibid.databases url = Option('url', 'URL of Trac instance') source = Option('source', 'Source to send commit notifications to') channel = Option('channel', 'Channel to send commit notifications to') announce_changes = BoolOption('announce_changes', u'Announce changes to tickets', True) def __init__(self, name): Processor.__init__(self, name) RPC.__init__(self) self.log = logging.getLogger('plugins.trac') def get_ticket(self, id): session = ibid.databases.trac() ticket = session.query(Ticket).get(id) session.close() return ticket def remote_ticket_created(self, id): ticket = self.get_ticket(id) if not ticket: raise Exception(u"No such ticket") message = u'New %s in %s reported by %s: "%s" %sticket/%s' % (ticket.type, ticket.component, ticket.reporter, ticket.summary, self.url, ticket.id) ibid.dispatcher.send({'reply': message, 'source': self.source, 'target': self.channel}) self.log.info(u'Ticket %s created', id) return True def remote_ticket_changed(self, id, comment, author, old_values): if not self.announce_changes: return False ticket = self.get_ticket(id) if not ticket: raise Exception(u'No such ticket') changes = [] for field, old in old_values.items(): if hasattr(ticket, field): changes.append(u'%s: %s' % (field, getattr(ticket, field))) if comment: changes.append(u'comment: "%s"' % comment) message = u'Ticket %s (%s %s %s in %s for %s) modified by %s. %s' % (id, ticket.status, ticket.priority, ticket.type, ticket.component, ticket.milestone, author, u', '.join(changes)) ibid.dispatcher.send({'reply': message, 'source': self.source, 'target': self.channel}) self.log.info(u'Ticket %s modified', id) return True @match(r'^ticket\s+(\d+)$') def get(self, event, number): ticket = self.get_ticket(int(number)) if ticket: event.addresponse(u'Ticket %(id)s (%(status)s %(priority)s %(type)s in %(component)s for %(milestone)s) ' u'reported %(ago)s ago assigned to %(owner)s: "%(summary)s" %(url)sticket/%(id)s', { 'id': ticket.id, 'status': ticket.status, 'priority': ticket.priority, 'type': ticket.type, 'component': ticket.component, 'milestone': ticket.milestone, 'ago': ago(datetime.now() - datetime.fromtimestamp(ticket.time), 2), 'owner': ticket.owner, 'summary': ticket.summary, 'url': self.url, }) else: event.addresponse(u"No such ticket") @match(r"^(?:(my|\S+?(?:'s))\s+)?(?:(open|closed|new|assigned)\s+)?tickets(?:\s+for\s+(.+?))?$") def handle_list(self, event, owner, status, milestone): session = ibid.databases.trac() status = status or 'open' if status.lower() == 'open': statuses = (u'new', u'assigned', u'reopened') else: statuses = (status.lower(),) query = session.query(Ticket).filter(Ticket.status.in_(statuses)) if owner: if owner.lower() == 'my': owner = event.sender['nick'] else: owner = owner.lower().replace("'s", '') query = query.filter(func.lower(Ticket.owner)==(owner.lower())) if milestone: query = query.filter_by(milestone=milestone) tickets = query.order_by(Ticket.id).all() if len(tickets) > 0: event.addresponse(u', '.join(['%s (%s): "%s"' % (ticket.id, ticket.owner, ticket.summary) for ticket in tickets])) else: event.addresponse(u"No tickets found") session.close() # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/karma.py0000644000000000000000000001456511414117331015162 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import datetime import re import logging from ibid.config import BoolOption, IntOption, ListOption from ibid.db import IbidUnicode, DateTime, Integer, Table, Column, Base, \ VersionedSchema from ibid.plugins import Processor, match, handler, authorise features = {'karma': { 'description': u'Keeps track of karma for people and things.', 'categories': ('remember',), }} log = logging.getLogger('plugins.karma') class Karma(Base): __table__ = Table('karma', Base.metadata, Column('id', Integer, primary_key=True), Column('subject', IbidUnicode(64, case_insensitive=True), unique=True, nullable=False, index=True), Column('changes', Integer, nullable=False), Column('value', Integer, nullable=False), Column('time', DateTime, nullable=False), useexisting=True) class KarmaSchema(VersionedSchema): def upgrade_1_to_2(self): self.add_index(self.table.c.subject) def upgrade_2_to_3(self): self.alter_column(Column('subject', IbidUnicode(64), unique=True, nullable=False, index=True)) def upgrade_3_to_4(self): self.drop_index(self.table.c.subject) self.alter_column(Column('subject', IbidUnicode(64, case_insensitive=True), unique=True, nullable=False, index=True), force_rebuild=True) self.add_index(self.table.c.subject) __table__.versioned_schema = KarmaSchema(__table__, 4) def __init__(self, subject): self.subject = subject self.changes = 0 self.value = 0 self.time = datetime.utcnow() class Set(Processor): usage = u' (++|--|==|ftw|ftl) [[reason]]' feature = ('karma',) # Clashes with morse & math priority = 510 permission = u'karma' increase = ListOption('increase', 'Suffixes which indicate increased karma', ('++', 'ftw')) decrease = ListOption('decrease', 'Suffixes which indicate decreased karma', ('--', 'ftl')) neutral = ListOption('neutral', 'Suffixes which indicate neutral karma', ('==',)) reply = BoolOption('reply', 'Acknowledge karma changes', False) public = BoolOption('public', 'Only allow karma changes in public', True) ignore = ListOption('ignore', 'Karma subjects to silently ignore', ()) importance = IntOption('importance', 'Threshold for number of changes after' " which a karma won't be forgotten", 0) def setup(self): self.set.im_func.pattern = re.compile( r'^(.+?)\s*(%s)\s*(?:[[{(]+\s*(.+?)\s*[\]})]+)?$' % '|'.join( re.escape(token) for token in self.increase + self.decrease + self.neutral ), re.I) @handler @authorise(fallthrough=False) def set(self, event, subject, adjust, reason): if self.public and not event.public: event.addresponse(u'Karma must be done in public') return if subject.lower() in self.ignore: return karma = event.session.query(Karma).filter_by(subject=subject).first() if not karma: karma = Karma(subject) if adjust.lower() in self.increase: if subject.lower() == event.sender['nick'].lower(): event.addresponse(u"You can't karma yourself!") return karma.changes += 1 karma.value += 1 change = u'Increased' elif adjust.lower() in self.decrease: karma.changes += 1 karma.value -= 1 change = u'Decreased' else: karma.changes += 2 change = u'Increased and decreased' if karma.value == 0 and karma.changes <= self.importance: change = u'Forgotten (unimportant)' event.session.delete(karma) else: event.session.save_or_update(karma) event.session.commit() log.info(u"%s karma for '%s' by %s/%s (%s) because: %s", change, subject, event.account, event.identity, event.sender['connection'], reason) if self.reply: event.addresponse(True) else: event.processed = True class Get(Processor): usage = u"""karma for [reverse] karmaladder""" feature = ('karma',) @match(r'^karma\s+(?:for\s+)?(.+)$') def handle_karma(self, event, subject): karma = event.session.query(Karma).filter_by(subject=subject).first() if not karma: event.addresponse(u'nobody cares, dude') elif karma.value == 0: event.addresponse(u'%s has neutral karma', subject) else: event.addresponse(u'%(subject)s has karma of %(value)s', { 'subject': subject, 'value': karma.value, }) @match(r'^(reverse\s+)?karmaladder$') def ladder(self, event, reverse): karmas = event.session.query(Karma) \ .order_by(reverse and Karma.value.asc() or Karma.value.desc()) \ .limit(30).all() if karmas: event.addresponse(', '.join(['%s: %s (%s)' % (karmas.index(karma), karma.subject, karma.value) for karma in karmas])) else: event.addresponse(u"I don't really care about anything") class Forget(Processor): usage = u'forget karma for [[reason]]' feature = ('karma',) # Clashes with factoid priority = -10 permission = u'karmaadmin' @match(r'^forget\s+karma\s+for\s+(.+?)(?:\s*[[{(]+\s*(.+?)\s*[\]})]+)?$') @authorise(fallthrough=False) def forget(self, event, subject, reason): karma = event.session.query(Karma).filter_by(subject=subject).first() if not karma: karma = Karma(subject) event.addresponse(u"I was pretty ambivalent about %s, anyway", subject) event.session.delete(karma) event.session.commit() log.info(u"Forgot karma for '%s' by %s/%s (%s) because: %s", subject, event.account, event.identity, event.sender['connection'], reason) event.addresponse(True) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/__init__.py0000644000000000000000000002621611516407732015634 0ustar rootroot# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from copy import copy from datetime import timedelta from inspect import getargspec, getmembers, ismethod import logging import re from threading import Lock from twisted.spread import pb from twisted.web import resource try: from twisted.plugin import pluginPackagePaths except ImportError: # Not available in Twisted 2.5.0 in Ubuntu hardy # This is straight from twisted.plugin import os.path import sys def pluginPackagePaths(name): package = name.split('.') return [os.path.abspath(os.path.join(x, *package)) for x in sys.path if not os.path.exists(os.path.join(x, *package + ['__init__.py']))] import ibid from ibid.compat import json __path__ = pluginPackagePaths(__name__) + __path__ for cat, desc, weight in ( ('account', u'bot accounts and permissions', None), ('admin', u'administrative functions', None), ('calculate', u'calculations', 0), ('convert', u'conversions', 0), ('debug', u'debugging me', None), ('decide', u'decisions', -2), ('development', u'software development', 10), ('fun', u'silly fun stuff', 0), ('game', u'games', -2), ('lookup', u'looking things up', -10), ('monitor', u'monitoring things', -2), ('remember', u'remembering things', -5), ('web', u'browsing the Internet', 0), ('message', u'delivering messages', -5), ('south africa', u'South African stuff', 10), ('sysadmin', u'System Administration', 5), ): if cat not in ibid.categories: ibid.categories[cat] = { 'description': desc, 'weight': weight, } class Processor(object): """Base class for Ibid plugins. Processors receive events and (optionally) do things with them. Events are filtered in process() by to the following attributes: event_types: Only these types of events addressed: Require the bot to be addressed for public messages processed: Process events marked as already having been handled permission: The permission to check when calling @authorised handlers priority: Low priority Processors are handled first autoload: Load this Processor, when loading the plugin, even if not explicitly required in the configuration file """ event_types = (u'message',) addressed = True processed = False priority = 0 autoload = True _event_handlers = None _periodic_handlers = None __log = logging.getLogger('plugins') def __new__(cls, *args): if cls.processed and cls.priority == 0: cls.priority = 1500 for name, option in options.items(): new = copy(option) new.default = getattr(cls, name) setattr(cls, name, new) for attr, listname in ( ('handler', 'event'), ('periodic', 'periodic')): listname = '_Processor__%s_handlers' % listname if not hasattr(cls, listname): setattr(cls, listname, []) handlers = getattr(cls, listname) for name, item in cls.__dict__.iteritems(): if getattr(item, attr, False) and name not in handlers: handlers.append(name) return super(Processor, cls).__new__(cls) def __init__(self, name): self.name = name self.setup() def setup(self): "Apply configuration. Called on every config reload" for name, method in getmembers(self, ismethod): if hasattr(method, 'interval_config_key'): method.im_func.interval = timedelta( seconds=getattr(self, method.interval_config_key, 0)) def shutdown(self): pass def process(self, event): "Process a single event" if event.type == 'clock': for method in self._get_periodic_handlers(): self._run_periodic_handler(method, event) if event.type not in self.event_types: return if self.addressed and ('addressed' not in event or not event.addressed): return if not self.processed and event.processed: return found = False for method in self._get_event_handlers(): args = None if not hasattr(method, 'pattern'): found = True args = () elif hasattr(event, 'message'): found = True match = method.pattern.search( event.message[method.message_version]) if match is not None: args = match.groups() if args is not None: if (not getattr(method, 'auth_required', False) or auth_responses(event, self.permission)): method(event, *args) elif not getattr(method, 'auth_fallthrough', True): event.processed = True if not found: raise RuntimeError(u'No handlers found in %s' % self) return event def _get_event_handlers(self): "Find all the handlers (regex matching and blind)" for handler in self._event_handlers or self.__event_handlers: yield getattr(self, handler) def _get_periodic_handlers(self): "Find all the periodic handlers" for handler in self._periodic_handlers or self.__periodic_handlers: yield getattr(self, handler) def _run_periodic_handler(self, method, event): "Run a periodic handler, if appropriate" if (method.interval.seconds > 0 and not method.disabled and method.lock.acquire(0)): try: if method.last_called is None: # First call, set up initial_delay method.im_func.last_called = event.time elif event.time - method.last_called >= ( method.initial_delay or method.interval): method.im_func.initial_delay = None method.im_func.last_called = event.time name = u'%s.%s' % (self.__class__.__name__, method.__name__) try: self.__log.debug(u'Running periodic event: %s', name) method(event) if method.failing: self.__log.info(u'No longer failing: %s', name) method.im_func.failing = False except: if not method.failing: self.__log.exception(u'Periodic method failing: %s', name) method.im_func.failing = True else: self.__log.debug(u'Still failing: %s', name) finally: method.lock.release() # This is a bit yucky, but necessary since ibid.config imports Processor from ibid.config import BoolOption, IntOption options = { 'addressed': BoolOption('addressed', u'Only process events if bot was addressed'), 'processed': BoolOption('processed', u"Process events even if they've already been processed"), 'priority': IntOption('priority', u'Processor priority'), } def handler(function): "Wrapper: Handle all events" function.handler = True function.message_version = 'clean' return function def match(regex, version='clean'): "Wrapper: Handle all events where the message matches the regex" pattern = re.compile(regex, re.I | re.DOTALL) def wrap(function): function.handler = True function.pattern = pattern function.message_version = version return function return wrap def auth_responses(event, permission): """Mark an event as having required authorisation, and return True if the event sender has permission. """ if not ibid.auth.authorise(event, permission): event.complain = u'notauthed' return False return True def authorise(fallthrough=True): """Require the permission specified in Processer.permission for the sender On failure, flags the event for Complain to respond appropriatly. If fallthrough=False, set the processed Flag to bypass later plugins. """ def wrap(function): function.auth_required = True function.auth_fallthrough = fallthrough return function return wrap def periodic(interval=0, config_key=None, initial_delay=60): """Wrapper: Run this handler every interval seconds If a config_key is provided, the interval will be set in Processor.setup() """ def wrap(function): function.periodic = True function.disabled = False function.lock = Lock() function.last_called = None function.interval = timedelta(seconds=interval) function.initial_delay = timedelta(seconds=initial_delay) if config_key is not None: function.interval_config_key = config_key function.failing = False return function return wrap from ibid.source.http import templates class RPC(pb.Referenceable, resource.Resource): isLeaf = True def __init__(self): ibid.rpc[self.feature[0]] = self self.form = templates.get_template('plugin_form.html') self.list = templates.get_template('plugin_functions.html') def get_function(self, request): method = request.postpath[0] if hasattr(self, 'remote_%s' % method): return getattr(self, 'remote_%s' % method) return None def render_POST(self, request): args = [] for arg in request.postpath[1:]: try: arg = json.loads(arg) except ValueError, e: pass args.append(arg) kwargs = {} for key, value in request.args.items(): try: value = json.loads(value[0]) except ValueError, e: value = value[0] kwargs[key] = value function = self.get_function(request) if not function: return "Not found" try: result = function(*args, **kwargs) return json.dumps(result) except Exception, e: return json.dumps({'exception': True, 'message': unicode(e)}) def render_GET(self, request): function = self.get_function(request) if not function: functions = [] for name, method in getmembers(self, ismethod): if name.startswith('remote_'): functions.append(name.replace('remote_', '', 1)) return self.list.render(object=self.feature[0], functions=functions) \ .encode('utf-8') args, varargs, varkw, defaults = getargspec(function) del args[0] if len(args) == 0 or len(request.postpath) > 1 or len(request.args) > 0: return self.render_POST(request) return self.form.render(args=args).encode('utf-8') # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/calc.py0000644000000000000000000001635311414117331014766 0ustar rootroot# Copyright (c) 2009-2010, Michael Gorven, Stefano Rivera, Jonathan Hitchcock # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from __future__ import division import logging from os import kill from random import random, randint from signal import SIGTERM from subprocess import Popen, PIPE from time import time, sleep from ibid.compat import all, factorial from ibid.config import Option, FloatOption from ibid.plugins import Processor, match from ibid.utils import file_in_path, unicode_output try: from ast import NodeTransformer, Pow, Name, Load, Call, copy_location, parse transform_method='ast' except ImportError: from compiler import ast, pycodegen, parse, misc, walk transform_method='compiler' class NodeTransformer(object): pass # ExpressionCodeGenerator doesn't inherit __futures__ from calling module: class FD_ExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator): futures = ('division',) features = {} log = logging.getLogger('calc') features['bc'] = { 'description': u'Calculate mathematical expressions using bc', 'categories': ('calculate',), } class BC(Processor): usage = u'bc ' feature = ('bc',) bc = Option('bc', 'Path to bc executable', 'bc') bc_timeout = FloatOption('bc_timeout', 'Maximum BC execution time (sec)', 2.0) def setup(self): if not file_in_path(self.bc): raise Exception("Cannot locate bc executable") @match(r'^bc\s+(.+)$') def calculate(self, event, expression): bc = Popen([self.bc, '-l'], stdin=PIPE, stdout=PIPE, stderr=PIPE) start_time = time() bc.stdin.write(expression.encode('utf-8') + '\n') bc.stdin.close() while bc.poll() is None and time() - start_time < self.bc_timeout: sleep(0.1) if bc.poll() is None: kill(bc.pid, SIGTERM) event.addresponse(u'Sorry, that took too long. I stopped waiting') return output = bc.stdout.read() error = bc.stderr.read() code = bc.wait() if code == 0: if output: output = unicode_output(output.strip()) output = output.replace('\\\n', '') event.addresponse(output) else: error = unicode_output(error.strip()) error = error.split(":", 1)[1].strip() error = error[0].lower() + error[1:].split('\n')[0] event.addresponse(u"I'm sorry, I couldn't deal with the %s", error) else: event.addresponse(u"Error running bc") error = unicode_output(error.strip()) raise Exception("BC Error: %s" % error) features['calc'] = { 'description': u'Returns the anwser to mathematical expressions. ' u'Uses Python syntax and semantics (i.e. radians)', 'categories': ('calculate',), } class LimitException(Exception): pass class AccessException(Exception): pass def limited_pow(*args): "We don't want users to DOS the bot. Pow is the most dangerous function. Limit it" # Large modulo-powers are ok, but otherwise we don't want enormous operands if (all(isinstance(arg, (int, long)) for arg in args) and not (len(args) == 3 and args[2] < 1e100)): try: answer = pow(float(args[0]), float(args[1])) if answer > 1e+300: raise LimitException except OverflowError, e: raise LimitException(e) return pow(*args) def limited_factorial(x): if x >= 500: raise LimitException return factorial(x) # ast method class PowSubstitutionTransformer(NodeTransformer): def visit_BinOp(self, node): self.generic_visit(node) if isinstance(node.op, Pow): fnode = Name('pow', Load()) copy_location(fnode, node) cnode = Call(fnode, [node.left, node.right], [], None, None) copy_location(cnode, node) return cnode return node def visit_Attribute(self, node): raise AccessException # compiler method class PowSubstitutionWalker(object): def visitPower(self, node, *args): walk(node.left, self) walk(node.right, self) cnode = ast.CallFunc(ast.Name('pow'), [node.left, node.right], None, None) node.left = cnode # Little hack: instead of trying to turn node into a CallFunc, we just do pow(left, right)**1 node.right = ast.Const(1) def visitGetattr(self, node, *args): raise AccessException class Calc(Processor): usage = u'' feature = ('calc',) priority = 500 extras = ('abs', 'round', 'min', 'max') banned = ('for', 'yield', 'lambda', '__', 'is') # Create a safe dict to pass to eval() as locals safe = {} exec('from math import *', safe) del safe['__builtins__'] for function in extras: safe[function] = eval(function) safe['pow'] = limited_pow safe['factorial'] = limited_factorial @match(r'^(.+)$') def calculate(self, event, expression): for term in self.banned: if term in expression: return try: # We need to remove all power operators and replace with our limited pow # ast is the new method (Python >=2.6) compiler is the old ast = parse(expression, mode='eval') if transform_method == 'ast': ast = PowSubstitutionTransformer().visit(ast) code = compile(ast, '', 'eval') else: misc.set_filename('', ast) walk(ast, PowSubstitutionWalker()) code = FD_ExpressionCodeGenerator(ast).getCode() result = eval(code, {'__builtins__': None}, self.safe) except ZeroDivisionError, e: event.addresponse(u"I can't divide by zero.") return except ArithmeticError, e: event.addresponse(u"I can't do that: %s", unicode(e)) return except ValueError, e: if unicode(e) == u"math domain error": event.addresponse(u"I can't do that: %s", unicode(e)) return except LimitException, e: event.addresponse(u"I'm afraid I'm not allowed to play with big numbers") return except Exception, e: return if isinstance(result, (int, long, float, complex)): event.addresponse(unicode(result)) class ExplicitCalc(Calc): usage = u'calc ' priority = 0 @match(r'^calc(?:ulate)?\s+(.+)$') def calculate(self, event, expression): super(ExplicitCalc, self).calculate(event, expression) features['random'] = { 'description': u'Generates random numbers.', 'categories': ('calculate', 'fun',), } class Random(Processor): usage = u'random [ | ]' feature = ('random',) @match('^rand(?:om)?(?:\s+(\d+)(?:\s+(\d+))?)?$') def random(self, event, begin, end): if not begin and not end: event.addresponse(u'I always liked %f', random()) else: begin = int(begin) end = end and int(end) or 0 event.addresponse(u'I always liked %i', randint(min(begin,end), max(begin,end))) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/social.py0000644000000000000000000001444211531035617015341 0ustar rootroot# Copyright (c) 2008-2011, Michael Gorven, Stefano Rivera, Jonathan Hitchcock # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from urllib2 import HTTPError from time import time from datetime import datetime import re import logging import feedparser from ibid.compat import ElementTree from ibid.config import DictOption from ibid.plugins import Processor, match, handler from ibid.utils import ago, decode_htmlentities, generic_webservice, \ json_webservice, parse_timestamp log = logging.getLogger('plugins.social') features = {} features['lastfm'] = { 'description': u'Lists the tracks last listened to by the specified user.', 'categories': ('lookup', 'web',), } class LastFm(Processor): usage = u'last.fm for ' feature = ('lastfm',) @match(r'^last\.?fm\s+for\s+(\S+?)\s*$') def listsongs(self, event, username): songs = feedparser.parse('http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.rss?%s' % (username, time())) if songs['bozo']: event.addresponse(u'No such user') else: event.addresponse(u', '.join(u'%s (%s ago)' % ( e.title, ago(event.time - parse_timestamp(e.updated)) ) for e in songs['entries'])) features['microblog'] = { 'description': u'Looks up messages on microblogging services like twitter ' u'and identica.', 'categories': ('lookup', 'web',), } class Twitter(Processor): usage = u"""latest (tweet|identica) from (tweet|identica) """ feature = ('microblog',) default = { 'twitter': {'endpoint': 'http://twitter.com/', 'api': 'twitter', 'name': 'tweet', 'user': 'twit'}, 'tweet': {'endpoint': 'http://twitter.com/', 'api': 'twitter', 'name': 'tweet', 'user': 'twit'}, 'identica': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, 'identi.ca': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, 'dent': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, } services = DictOption('services', 'Micro blogging services', default) class NoTweetsException(Exception): pass def setup(self): self.update.im_func.pattern = re.compile(r'^(%s)\s+(\d+)$' % '|'.join(self.services.keys()), re.I) self.latest.im_func.pattern = re.compile(r'^(?:latest|last)\s+(%s)\s+(?:update\s+)?(?:(?:by|from|for)\s+)?@?(\S+)$' % '|'.join(self.services.keys()), re.I) def remote_update(self, service, id): status = json_webservice('%sstatuses/show/%s.json' % (service['endpoint'], id)) return {'screen_name': status['user']['screen_name'], 'text': decode_htmlentities(status['text'])} def remote_latest(self, service, user): if service['api'] == 'twitter': # Twitter ommits retweets in the JSON and XML results: statuses = generic_webservice('%sstatuses/user_timeline/%s.atom' % (service['endpoint'], user.encode('utf-8')), {'count': 1}) tree = ElementTree.fromstring(statuses) latest = tree.find('{http://www.w3.org/2005/Atom}entry') if latest is None: raise self.NoTweetsException(user) return { 'text': latest.findtext('{http://www.w3.org/2005/Atom}content') .split(': ', 1)[1], 'ago': ago(datetime.utcnow() - parse_timestamp( latest.findtext('{http://www.w3.org/2005/Atom}published'))), 'url': [x for x in latest.getiterator('{http://www.w3.org/2005/Atom}link') if x.get('type') == 'text/html' ][0].get('href'), } elif service['api'] == 'laconica': statuses = json_webservice('%sstatuses/user_timeline/%s.json' % (service['endpoint'], user.encode('utf-8')), {'count': 1}) if not statuses: raise self.NoTweetsException(user) latest = statuses[0] url = '%s/notice/%i' % (service['endpoint'].split('/api/', 1)[0], latest['id']) return { 'text': decode_htmlentities(latest['text']), 'ago': ago(datetime.utcnow() - parse_timestamp(latest['created_at'])), 'url': url, } @handler def update(self, event, service_name, id): service = self.services[service_name.lower()] try: event.addresponse(u'%(screen_name)s: "%(text)s"', self.remote_update(service, int(id))) except HTTPError, e: if e.code in (401, 403): event.addresponse(u'That %s is private', service['name']) elif e.code == 404: event.addresponse(u'No such %s', service['name']) else: log.debug(u'%s raised %s', service['name'], unicode(e)) event.addresponse(u'I can only see the Fail Whale') @handler def latest(self, event, service_name, user): service = self.services[service_name.lower()] try: event.addresponse(u'"%(text)s" %(ago)s ago, %(url)s', self.remote_latest(service, user)) except HTTPError, e: if e.code in (401, 403): event.addresponse(u"Sorry, %s's feed is private", user) elif e.code == 404: event.addresponse(u'No such %s', service['user']) else: log.debug(u'%s raised %s', service['name'], unicode(e)) event.addresponse(u'I can only see the Fail Whale') except self.NoTweetsException, e: event.addresponse( u'It appears that %(user)s has never %(tweet)sed', { 'user': user, 'tweet': service['name'], }) @match(r'^https?://(?:www\.)?twitter\.com/(?:#!/)?[^/ ]+/statuse?s?/(\d+)$') def twitter(self, event, id): self.update(event, u'twitter', id) @match(r'^https?://(?:www\.)?identi.ca/notice/(\d+)$') def identica(self, event, id): self.update(event, u'identica', id) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/games.py0000644000000000000000000007600211414117331015155 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera, Max Rabkin # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from datetime import timedelta import logging from random import choice, gauss, random, shuffle import re import ibid from ibid.compat import defaultdict from ibid.config import IntOption, BoolOption, FloatOption, ListOption, DictOption from ibid.plugins import Processor, match, handler from ibid.utils import format_date, human_join, plural features = {} log = logging.getLogger('plugins.games') duels = {} features['duel'] = { 'description': u'Duel at dawn, between channel members', 'categories': ('fun', 'game',), } class DuelInitiate(Processor): usage = u""" I challenge to a duel [over ] I demand satisfaction from [over ] I throw the gauntlet down at 's feet [over ] """ feature = ('duel',) accept_timeout = FloatOption('accept_timeout', 'How long do we wait for acceptance?', 60.0) start_delay = IntOption('start_delay', 'Time between acceptance and start of duel (rounded down to the highest minute)', 30) timeout = FloatOption('timeout', 'How long is a duel on for', 15.0) happy_endings = ListOption('happy_endings', 'Both survive', ( u'walk off into the sunset', u'go for a beer', u'call it quits', )) class Duel(object): def stop(self): for callback in ('cancel', 'start', 'timeout'): callback += '_callback' if hasattr(self, callback) and getattr(self, callback).active(): getattr(self, callback).cancel() def shutdown(self): for duel in duels: duel.stop() @match(r'^(?:I\s+)throw\s+(?:down\s+(?:the|my)\s+gauntlet|(?:the|my)\s+gauntlet\s+down)\s+' r'at\s+(\S+?)(?:\'s\s+feet)?(?:\s+(?:over|because|for)\s+.+)?$') def initiate_gauntlet(self, event, recipient): self.initiate(event, recipient) @match(r'^(?:I\s+)?demand\s+satisfaction\s+from\s+(\S+)(?:\s+(?:over|because|for)\s+.+)?$') def initiate_satisfaction(self, event, recipient): self.initiate(event, recipient) @match(r'^(?:I\s+)?challenge\s+(\S+)(?:\s+to\s+a\s+duel)?(?:\s+(?:over|because|for)\s+.+)?$') def initiate(self, event, recipient): if not event.public: event.addresponse(choice(( u"All duels must take place in public places, by decree of the bot", u"How do you expect to fight %(recipient)s, when he is not present?", u"Your challenge must be made in public, Sir Knight", )), { 'recipient': recipient }) return if (event.source, event.channel) in duels: event.addresponse(choice(( u"We already have a war in here. Take your fight outside", u"Isn't one fight enough? You may wait your turn", ))) return aggressor = event.sender['nick'] if recipient.lower() == aggressor.lower(): # Yes I know schizophrenia isn't the same as DID, but this sounds better :P event.addresponse(choice(( u"Are you schizophrenic?", u"Um, How exactly do you plan on fighting yourself?", ))) return if recipient.lower() in [name.lower() for name in ibid.config.plugins['core']['names']]: event.addresponse(choice(( u"I'm a peaceful bot", u"The ref can't take part in the battle", u"You just want me to die. No way", ))) return duel = self.Duel() duels[(event.source, event.channel)] = duel duel.hp = { aggressor.lower(): 100.0, recipient.lower(): 100.0, } duel.names = { aggressor.lower(): aggressor, recipient.lower(): recipient, } duel.drawn = { aggressor.lower(): False, recipient.lower(): False, } duel.started = False duel.confirmed = False duel.aggressor = event.sender['nick'].lower() duel.recipient = recipient.lower() duel.cancel_callback = ibid.dispatcher.call_later(self.accept_timeout, self.cancel, event) event.addresponse(u'%(recipient)s: ' + choice(( u"The gauntlet has been thrown at your feet. Do you accept?", u"You have been challenged. Do you accept?", u"%(aggressor)s wishes to meet you at dawn on the field of honour. Do you accept?", )), { 'recipient': recipient, 'aggressor': event.sender['nick'], }, address=False) def cancel(self, event): duel = duels[(event.source, event.channel)] del duels[(event.source, event.channel)] event.addresponse(choice(( u"%(recipient)s appears to have fled the country during the night", u"%(recipient)s refuses to meet your challenge and accepts dishonour", u"Your challenge was not met. I suggest anger management counselling", )), { 'recipient': duel.names[duel.recipient], }) @match(r'^.*\b(?:ok|yes|I\s+do|sure|accept|hit\s+me|bite\s+me|i\'m\s+game|bring\s+it|yebo)\b.*$') def confirm(self, event): if (event.source, event.channel) not in duels: return duel = duels[(event.source, event.channel)] confirmer = event.sender['nick'].lower() if confirmer not in duel.names or duel.confirmed or confirmer != duel.recipient: return # Correct capitalisation duel.names[confirmer] = event.sender['nick'] duel.confirmed = True duel.cancel_callback.cancel() starttime = event.time + timedelta( seconds=self.start_delay + ((30 - event.time.second) % 30)) starttime = starttime.replace(microsecond=0) delay = starttime - event.time delay = delay.seconds + (delay.microseconds / 10.**6) duel.start_callback = ibid.dispatcher.call_later(delay, self.start, event) event.addresponse(u"%(aggressor)s, %(recipient)s: " u"The duel shall begin on the stroke of %(starttime)s (in %(delay)s seconds). " + choice(( u"You may clean your pistols.", u"Prepare yourselves.", u"Get ready", )), { 'aggressor': duel.names[duel.aggressor], 'recipient': duel.names[duel.recipient], 'starttime': format_date(starttime, 'time'), 'delay': (starttime - event.time).seconds, }, address=False) def start(self, event): duel = duels[(event.source, event.channel)] duel.started = True duel.timeout_callback = ibid.dispatcher.call_later(self.timeout, self.end, event) event.addresponse(u'%s, %s: %s' % tuple(duel.names.values() + [choice(( u'aaaand ... go!', u'5 ... 4 ... 3 ... 2 ... 1 ... fire!', u'match on!', u'ready ... aim ... fire!' ))]), address=False) def end(self, event): duel = duels[(event.source, event.channel)] del duels[(event.source, event.channel)] winner, loser = duel.names.keys() if duel.hp[winner] < duel.hp[loser]: winner, loser = loser, winner if duel.hp[loser] == 100.0: message = u"DRAW: %(winner)s and %(loser)s shake hands and %(ending)s" elif duel.hp[winner] < 50.0: message = u"DRAW: %(winner)s and %(loser)s bleed to death together" elif duel.hp[loser] < 50.0: message = u"VICTORY: %(loser)s bleeds to death" elif duel.hp[winner] < 100.0: message = u"DRAW: %(winner)s and %(loser)s hobble off together. Satisfaction is obtained" else: message = u"VICTORY: %(loser)s hobbles off while %(winner)s looks victorious" event.addresponse(message, { 'loser': duel.names[loser], 'winner': duel.names[winner], 'ending': choice(self.happy_endings), }, address=False) class DuelDraw(Processor): usage = u"""draw [my ] bam|pew|bang|kapow|pewpew|holyhandgrenadeofantioch""" feature = ('duel',) # Parameters for Processor: event_types = (u'message', u'action') addressed = BoolOption('addressed', 'Must the bot be addressed?', True) # Game configurables: weapons = DictOption('weapons', 'Weapons that can be used: name: (chance, damage)', { u'bam': (0.75, 50), u'pew': (0.75, 50), u'fire': (0.75, 70), u'fires': (0.75, 70), u'bang': (0.75, 70), u'kapow': (0.75, 90), u'pewpew': (0.75, 110), u'holyhandgrenadeofantioch': (1.0, 200), }) extremities = ListOption('extremities', u'Extremities that can be hit', ( u'toe', u'foot', u'leg', u'thigh', u'finger', u'hand', u'arm', u'elbow', u'shoulder', u'ear', u'nose', u'stomach', )) vitals = ListOption('vitals', 'Vital parts of the body that can be hit', ( u'head', u'groin', u'chest', u'heart', u'neck', )) draw_required = BoolOption('draw_required', 'Must you draw your weapon before firing?', True) extratime = FloatOption('extratime', 'How much more time to grant after every shot fired?', 1.0) @match(r'^draws?(?:\s+h(?:is|er)\s+.*|\s+my\s+.*)?$') def draw(self, event): if (event.source, event.channel) not in duels: if event.get('addressed', False): event.addresponse(choice(( u"We do not permit drawn weapons here", u"You may only draw a weapon on the field of honour", ))) return duel = duels[(event.source, event.channel)] shooter = event.sender['nick'] if shooter.lower() not in duel.names: event.addresponse(choice(( u"Spectators are not permitted to draw weapons", u"Do you think you are %(fighter)s?", )), {'fighter': choice(duel.names.values())}) return if not duel.started: event.addresponse(choice(( u"Now now, not so fast!", u"Did I say go yet?", u"Put that AWAY!", ))) return duel.drawn[shooter.lower()] = True event.addresponse(True) def setup(self): self.fire.im_func.pattern = re.compile( r'^(%s)(?:[\s,.!:;].*)?$' % '|'.join(self.weapons.keys()), re.I | re.DOTALL) @handler def fire(self, event, weapon): shooter = event.sender['nick'].lower() if (event.source, event.channel) not in duels: return duel = duels[(event.source, event.channel)] if shooter not in duel.names: event.addresponse(choice(( u"You aren't in a war", u'You are a non-combatant', u'You are a spectator', ))) return enemy = set(duel.names.keys()) enemy.remove(shooter) enemy = enemy.pop() if self.draw_required and not duel.drawn[shooter]: recipient = shooter else: recipient = enemy if not duel.started or not duel.confirmed: if self.draw_required: message = choice(( u"%(shooter)s tried to escape his duel by shooting himself in the foot. The duel has been cancelled, but his honour is forfeit", u"%(shooter)s shot himself while preparing for his duel. The funeral will be held on the weekend", )) elif not duel.started: message = choice(( u"FOUL! %(shooter)s fired before my mark. Just as well you didn't hit anything. I refuse to referee under these conditions", u"FOUL! %(shooter)s injures %(enemy)s before the match started and is marched away in handcuffs", u"FOUL! %(shooter)s killed %(enemy)s before the match started and was shot by the referee before he could hurt anyone else", )) else: message = choice(( u"FOUL! The duel is not yet confirmed. %(shooter)s is marched away in handcuffs", u"FOUL! Arrest %(shooter)s! Firing a weapon within city limits is not permitted", )) event.addresponse(message, { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], }, address=False) del duels[(event.source, event.channel)] duel.stop() return chance, power = self.weapons[weapon.lower()] if random() < chance: damage = max(gauss(power, power/2.0), 0) duel.hp[recipient] -= damage if duel.hp[recipient] <= 0.0: del duels[(event.source, event.channel)] duel.stop() else: duel.timeout_callback.delay(self.extratime) params = { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], 'part': u'foot', } if shooter == recipient: message = u"TRAGEDY: %(shooter)s shoots before drawing his weapon. " if damage > 100.0: message += choice(( u"The explosion killed him", u"There was little left of him", )) elif duel.hp[recipient] <= 0.0: message += choice(( u"Combined with his other injuries, he didn't stand a chance", u"He died during field surgery", )) else: message += choice(( u"Luckily, it was only a flesh wound", u"He narrowly missed his femoral artery", )) elif damage > 100.0: message = u'VICTORY: ' + choice(( u'%(shooter)s blows %(enemy)s away', u'%(shooter)s destroys %(enemy)s', )) elif duel.hp[enemy] <= 0.0: message = u'VICTORY: ' + choice(( u'%(shooter)s kills %(enemy)s with a shot to the %(part)s', u'%(shooter)s shoots %(enemy)s killing him with a fatal shot to the %(part)s', )) params['part'] = choice(self.vitals) else: message = choice(( u'%(shooter)s hits %(enemy)s in the %(part)s, wounding him', u'%(shooter)s shoots %(enemy)s in the %(part)s, but %(enemy)s can still fight', )) params['part'] = choice(self.extremities) event.addresponse(message, params, address=False) elif shooter == recipient: event.addresponse(choice(( u"%s forget to draw his weapon. Luckily he missed his foot", u"%s fires a holstered weapon. Luckily it only put a hole in his jacket", u"%s won't win at this rate. He forgot to draw before firing. He missed himself too", )), duel.names[shooter], address=False) else: event.addresponse(choice(( u'%s misses', u'%s aims wide', u'%s is useless with a weapon' )), duel.names[shooter], address=False) class DuelFlee(Processor): feature = ('duel',) addressed = False event_types = (u'state',) @handler def dueller_fled(self, event): if event.state != 'offline': return fleer = event.sender['nick'].lower() for (source, channel), duel in duels.items(): if source != event.source or fleer not in duel.names: continue if hasattr(event, 'othername'): newnamekey = event.othername.lower() for key in ('hp', 'names', 'drawn'): getattr(duel, key)[newnamekey] = getattr(duel, key)[fleer] del getattr(duel, key)[fleer] duel.names[newnamekey] = event.othername if duel.aggressor == fleer: duel.aggressor = newnamekey else: duel.recipient = newnamekey event.addresponse(choice(( "%s: Changing your identity won't help", "%s: You think I didn't see that?", "%s: There's no escape, you know", )), event.othername, target=channel, address=False) else: del duels[(source, channel)] duel.stop() event.addresponse(choice(( "VICTORY: %(winner)s: %(fleer)s has fled the country during the night", "VICTORY: %(winner)s: The cowardly %(fleer)s has run for his life", )), { 'winner': duel.names[[name for name in duel.names if name != fleer][0]], 'fleer': duel.names[fleer], }, target=channel) werewolf_games = [] features['werewolf'] = { 'description': u'Play the werewolf game. Channel becomes a village ' u'containing a werewolf, seer and villagers. Every night, ' u'the werewolf can kill a villager, and the seer can test ' u'a villager for werewolf symptoms. Villagers then vote to ' u'lynch a wolf during the day.', 'categories': ('fun', 'game',), } class WerewolfGame(Processor): usage = u""" start a game of werewolf join ( kill | see | eat ) vote for """ feature = ('werewolf',) state = None player_limit = IntOption('min_players', 'The minimum number of players', 5) start_delay = IntOption('start_delay', 'How long to wait before starting, in seconds', 60) day_length = IntOption('day_length', 'Length of day / night, in seconds', 60) addressed = BoolOption('addressed', 'Messages must be addressed to bot', True) players_per_wolf = IntOption('players_per_wolf', 'Number of players to each wolf/seer', 4) seer_delay = IntOption('seer_delay', 'Number of players between extra wolf and extra seer', 4) event_types = (u'message', u'action') @match(r'^(?:start|play|begin)s?\b.*werewolf$') def prestart(self, event): """Initiate a game. This is the state from initiation to start of game. Next state is start. """ if self.state: log.debug(u'Not starting game: already in state %s.', self.state_name()) return if not event.public: log.debug(u'Event is not public.') event.addresponse(u'You must start the game in public.') return self.state = self.prestart self.channel = event.channel log.debug(u'Starting game.') werewolf_games.append(self) starter = event.sender['nick'] self.players = set((starter,)) event.addresponse(u'You have started a game of Werewolf. ' u'Everybody has %i seconds to join the game.', self.start_delay) self.timed_goto(event, self.start_delay, self.start) @match(r'^joins?\b') def join(self, event): if self.state != self.prestart: log.debug(u'Not joining: already in state %s.', self.state_name()) return if event.sender['nick'] not in self.players: self.players.add(event.sender['nick']) event.addresponse(u'%(player)s has joined (%(num)i players).', { 'num': len(self.players), 'player': event.sender['nick'] }, target=self.channel, address=False) else: event.addresponse(u'You have already joined the game.') def start(self, event): """Start game. Players are assigned their roles. The next state is night. """ self.state = self.start if len(self.players) < self.player_limit: event.addresponse(u'Not enough players. Try again later.') self.state = None return event.addresponse( u'%i players joined. Please wait while I assign roles.', len(self.players)) self.players = list(self.players) shuffle(self.players) nwolves = max(1, len(self.players) // self.players_per_wolf) nseers = max(1, (len(self.players) - self.seer_delay) // self.players_per_wolf) self.wolves = set(self.players[:nwolves]) self.seers = set(self.players[nwolves:nwolves + nseers]) self.roles = dict((player, 'villager') for player in self.players) del self.players for player in self.wolves: self.roles[player] = 'wolf' for player in self.seers: self.roles[player] = 'seer' for player, role in self.roles.iteritems(): event.addresponse(u'%(name)s, you are a %(role)s.', { 'name': player, 'role': role, }, target=player, address=False) if nwolves > 1 and nseers > 1: event.addresponse( u'This game has %(seers)i seers and %(wolves)i wolves.', { 'seers': nseers, 'wolves': nwolves, }) elif nwolves > 1: event.addresponse(u'This game has %i wolves.', nwolves) elif nseers > 1: event.addresponse(u'This game has %i seers.', nseers) self.timed_goto(event, 10, self.night) def night(self, event): """Start of night. Tell seer and werewolf to act. This state lasts for the whole night. The next state is dawn. """ self.state = self.night event.addresponse( u'Night falls... most villagers are sleeping, ' u'but outside, something stirs.\n' + plural(len(self.wolves), u'Werewolf, you may kill somebody.', u'Werewolves, you may kill somebody.') + '\n' + plural(len(self.seers), u"Seer, you may discover somebody's true form.", u"Seers, you may discover somebody's true form."), conflate=False) self.say_survivors(event) self.wolf_targets = {} self.seer_targets = {} self.timed_goto(event, self.day_length, self.dawn) @match(r'^(?:kill|see|eat)s?\s+(\S+)$') def kill_see(self, event, target_nick): """Kill or see a player. Only works for seers and wolves. """ if (self.state != self.night or event.public or event.sender['nick'] not in self.roles): return sender = event.sender['nick'] target = self.identify(target_nick) if target is None: event.addresponse(u'%s is not playing.', target_nick) elif self.roles[sender] == 'wolf': event.addresponse(u'You have chosen %s for your feast tonight.', target_nick) self.wolf_targets[sender] = target elif self.roles[sender] == 'seer': event.addresponse(u"You will discover %s's role at dawn tomorrow.", target_nick) self.seer_targets[sender] = target def dawn(self, event): """Start of day. During this state, villagers discover what happened overnight and discuss who to lynch. The next state is noon. """ self.state = self.dawn eaten = frozenset(self.wolf_targets.itervalues()) if eaten: victim = choice(list(eaten)) event.addresponse( u'The village awakes to find that werewolves have ' u'devoured %(nick)s the %(role)s in the night.', { 'nick': victim, 'role': self.roles[victim], }) self.death(victim) else: event.addresponse(u'The werewolves were abroad last night.') self.wolf_targets = {} for seer in self.seers: target = self.seer_targets.get(seer) if target is not None: # seer saw somebody if target in self.roles: # that somebody is alive msg = u'%(nick)s is a %(role)s' % { 'nick': target, 'role': self.roles[target], } else: msg = u'The wolves also had %s in mind last night.' \ % target event.addresponse(msg, target=seer) self.seer_targets = {} if not self.endgame(event): event.addresponse(u'Villagers, you have %i seconds ' u'to discuss suspicions and cast accusations.', self.day_length) self.say_survivors(event) self.timed_goto(event, self.day_length, self.noon) def noon(self, event): """Start of voting. Next state is dusk. """ self.state = self.noon event.addresponse(u'Villagers, you have %i seconds to cast ' u'your vote to lynch somebody.', self.day_length) self.votes = {} self.timed_goto(event, self.day_length, self.dusk) @match(r'^(?:lynch(?:es)?|votes?)\s+(?:for|against)\s+(\S+)$') def vote(self, event, target_nick): """Vote to lynch a player.""" if (self.state != self.noon or event.sender['nick'] not in self.roles): return target = self.identify(target_nick) if target is None: event.addresponse(u'%s is not playing.', target_nick) else: self.votes[event.sender['nick']] = target event.addresponse(u'%(voter)s voted for %(target)s.', { 'target': target, 'voter': event.sender['nick'], }, target=self.channel, address=False) def dusk(self, event): """Counting of votes and lynching. Next state is night. """ self.state = self.dusk vote_counts = defaultdict(int) for vote in self.votes.values(): vote_counts[vote] += 1 self.votes = {} victims = [] victim_votes = 0 for player, votes in vote_counts.iteritems(): if votes > victim_votes: victims = [player] victim_votes = votes elif votes == victim_votes: victims.append(player) if victims: if len(victims) > 1: event.addresponse(u'The votes are tied. Picking randomly...') victim = choice(victims) event.addresponse(u'The ballots are in, ' u'and %(nick)s the %(role)s has been lynched.', { 'nick': victim, 'role': self.roles[victim], }) self.death(victim) else: event.addresponse(u'Nobody voted.') if not self.endgame(event): self.timed_goto(event, 10, self.night) def say_survivors(self, event): """Name surviving players.""" event.addresponse(u'The surviving villagers are: %s.', human_join(self.roles)) def identify(self, nick): """Find the identity (correctly-capitalised nick) of a player. Returns None if nick is not playing. """ for player in self.roles.iterkeys(): if player.lower() == nick.lower(): return player return None def death(self, player): """Remove player from game.""" if self.state == self.prestart: self.players.remove(player) elif self.state is not None: del self.roles[player] for role in (self.wolves, self.seers): try: role.remove(player) except KeyError: pass def endgame(self, event): """Check if the game is over. If the game is over, announce the winners and return True. Otherwise return False. """ if 2 * len(self.wolves) >= len(self.roles): # werewolves win event.addresponse(u'The werewolves devour the remaining ' u'villagers and win. OM NOM NOM.\n' u'The winning werewolves were: %s', human_join(self.wolves), conflate=False) elif not self.wolves: # villagers win event.addresponse(u'The villagers have defeated the werewolves. ' u'Vigilantism FTW.\n' u'The surviving villagers were: %s', human_join(self.roles), conflate=False) else: return False self.state = None werewolf_games.remove(self) return True def timed_goto(self, event, delay, target): """Like call_later, but does nothing if state has changed.""" from_state = self.state log.debug(u'Going from state %s to %s in %i seconds.', self.state_name(), target.__name__, delay) def goto (evt): """Change state if it hasn't already changed.""" if self.state == from_state: target(evt) ibid.dispatcher.call_later(delay, goto, event) def rename(self, oldnick, newnick): """Rename a player.""" for playerset in ('players', 'wolves', 'seers'): if hasattr(self, playerset): playerset = getattr(self, playerset) if oldnick in playerset: playerset.remove(oldnick) playerset.add(newnick) if hasattr(self, 'roles') and oldnick in self.roles: self.roles[newnick] = self.roles[oldnick] del self.roles[oldnick] def state_change(self, event): if self.state is None: return if not hasattr(event, 'state'): return if event.state != 'online': nick = event.sender['nick'] if hasattr(event, 'othername'): self.rename(event.othername, nick) elif ((self.state == self.prestart and nick in self.players) or nick in self.roles): event.addresponse(u'%s has fled the game in terror.', nick, target=self.channel, address=False) self.death(nick) def state_name(self): "Return a printable version of the current state" if self.state is None: return 'stopped' return self.state.__name__ class WerewolfState(Processor): feature = ('werewolf',) event_types = (u'state',) @handler def state_change(self, event): for game in werewolf_games: game.state_change(event) # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/rfc.py0000644000000000000000000002113011414117331014623 0ustar rootroot# Copyright (c) 2009-2010, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. import logging import re import time from ibid.config import Option, IntOption from ibid.plugins import Processor, match from ibid.utils import cacheable_download features = {'rfc': { 'description': u'Looks up RFCs by number or title.', 'categories': ('lookup', 'web', 'development',), }} cachetime = 60*60 log = logging.getLogger("plugin.rfc") class RFCLookup(Processor): usage = u"""rfc rfc [for] rfc [for] /regex/""" feature = ('rfc',) indexurl = Option('index_url', "A HTTP url for the RFC Index file", "http://www.rfc-editor.org/rfc/rfc-index.txt") cachetime = IntOption("cachetime", "Time to cache RFC index for", cachetime) indexfile = None last_checked = 0 def _update_list(self): if not self.indexfile or time.time() - self.last_checked > self.cachetime: self.indexfile = cacheable_download(self.indexurl, "rfc/rfc-index.txt") self.last_checked = time.time() class RFC(object): special_authors = ( "Ed\.", "Eds\.", "RFC Editor", "IAP", "et al\.", "IAB", "IAB and IESG", "Internet Architecture Board", "Defense Advanced Research Projects Agency", "Internet Activities Board", "Gateway Algorithms and Data Structures Task Force", "International Organization for Standardization", "IAB Advisory Committee", "Federal Networking Council", "Internet Engineering Steering Group", "The Internet Society", "Sun Microsystems", "KOI8-U Working Group", "ISOC Board of Trustees", "Internet Assigned Numbers Authority \(IANA\)", "The North American Directory Forum", "Vietnamese Standardization Working Group", "ESnet Site Coordinating Comittee \(ESCC\)", "Energy Sciences Network \(ESnet\)", "North American Directory Forum", "Stanford Research Institute", "National Research Council", "Information Sciences Institute University of Southern California", "Bolt Beranek and Newman Laboratories", "International Telegraph and Telephone Consultative Committee of the International Telecommunication Union", "National Bureau of Standards", "Network Technical Advisory Group", "National Science Foundation", "End-to-End Services Task Force", "NetBIOS Working Group in the Defense Advanced Research Projects Agency", "ESCC X.500/X.400 Task Force", ) # She's pretty, isn't she? # Beginners guide: # First line is title, initials # Second is middle names, surnames, and suffixes # Third is date and extensions record_re = re.compile(r"^(.+?)\. ((?:(?:[A-Z]{1,2}|[A-Z]\.-?[A-Z]|[A-Z]-[A-Z]|[A-Z]\([A-Z]\)|[A-Z][a-z]+)\.{0,2}" r"(?: (?:[Vv]an|[Dd]e[nr]?|[Ll][ae]|El|Del|Dos|da))* ?[a-zA-Z\-']+(?:[\.,]? (?:\d+(?:rd|nd|st|th)|Jr|I+)\.?)?|%s)" r"(?:, ?)?)+\. ([A-Z][a-z]{2,8}(?: \d{1,2})? \d{4})\. \((.+)\)$" % "|".join(special_authors)) extensions_re = re.compile(r"\) \(") def __init__(self, number, record): self.number = number self.record = unicode(record, encoding="ASCII") self.issued = not self.record == "Not Issued." self.summary = self.record def parse(self): if self.issued: m = self.record_re.match(self.record) if not m: log.warning("CAN'T DECODE RFC: " + self.record) else: self.title, self.authors, self.date, extensions = m.groups() extensions = self.extensions_re.split(extensions) self.formats = [] self.status = None self.also = None self.obsoleted = self.obsoletes = None self.updated = self.updates = None self.online = True for ex in extensions: if ex.startswith("Format:"): self.formats = [fmt.strip() for fmt in ex.split(":", 1)[1].split(",")] elif ex.startswith("Status:"): self.status = ex.split(":", 1)[1].strip() elif ex == "Not online": self.online = False else: values = [fmt.strip() for fmt in ex.split(" ", 1)[1].split(",")] values = [val[:3] == "RFC" and val[3:] or val for val in values] if ex.startswith("Also"): self.also = values elif ex.startswith("Obsoleted by"): self.obsoleted = values elif ex.startswith("Obsoletes"): self.obsoletes = values elif ex.startswith("Updated by"): self.updated = values elif ex.startswith("Updates"): self.updates = values else: log.warning("CAN'T DECODE RFC: " + self.record) extensions = [":" in ex and ex.split(":", 1) or ex.split(" ", 1) for ex in extensions if ":" in ex] extensions = dict([(name.strip().upper(), values.strip()) for name, values in extensions]) self.extensions = extensions self.summary = u"%s. %s." % (self.title, self.date) if self.status: self.summary += u" " + self.status if self.obsoleted: self.summary += u" Obsoleted by " + u", ".join(self.obsoleted) def _parse_rfcs(self): self._update_list() f = file(self.indexfile, "rU") lines = f.readlines() f.close() breaks = 0 strip = -1 for lineno, line in enumerate(lines): if line.startswith(20 * "~"): breaks += 1 elif breaks == 2 and line.startswith("000"): strip = lineno break lines = lines[strip:] rfcs = {} buf = "" # So there's nothing left in buf: lines.append("") for line in lines: line = line.strip() if line: buf += " " + line elif buf: number, desc = buf.strip().split(None, 1) number = int(number) rfcs[number] = self.RFC(number, desc) buf = "" return rfcs @match(r'^rfc\s+#?(\d+)$') def lookup(self, event, number): rfcs = self._parse_rfcs() number = int(number) if number in rfcs: event.addresponse(u"%(record)s http://www.rfc-editor.org/rfc/rfc%(number)i.txt", { 'record': rfcs[number].record, 'number': number, }) else: event.addresponse(u"Sorry, no such RFC") @match(r'^rfc\s+(?:for\s+)?(.+)$') def search(self, event, terms): # If it's an RFC number, lookup() will catch it if terms.isdigit(): return rfcs = self._parse_rfcs() # Search engines: pool = rfcs.itervalues() if len(terms) > 2 and terms[0] == terms[-1] == "/": try: term_re = re.compile(terms[1:-1], re.I) except re.error: event.addresponse(u"Couldn't search. Invalid regex: %s", re.message) return pool = [rfc for rfc in pool if term_re.search(rfc.record)] else: terms = set(terms.split()) for term in terms: pool = [rfc for rfc in pool if term.lower() in rfc.record.lower()] # Newer RFCs matter more: pool.reverse() if pool: results = [] for result in pool[:5]: result.parse() results.append("%04i: %s" % (result.number, result.summary)) event.addresponse(u'Found %(found)i matching RFCs. Listing %(listing)i: %(results)s', { 'found': len(pool), 'listing': min(len(pool), 5), 'results': u', '.join(results), }) else: event.addresponse(u"Sorry, can't find anything") # vi: set et sta sw=4 ts=4: ibid-0.1.1/ibid/plugins/geography.py0000644000000000000000000003410011414117331016037 0ustar rootroot# Copyright (c) 2009-2010, Jonathan Hitchcock, Michael Gorven, Stefano Rivera # Released under terms of the MIT/X/Expat Licence. See COPYING for details. from math import acos, sin, cos, radians from urllib import quote from urlparse import urljoin import re import logging from os.path import exists, join from datetime import datetime from os import walk from dateutil.parser import parse from dateutil.tz import gettz, tzlocal, tzoffset from ibid.plugins import Processor, match from ibid.utils import json_webservice, human_join, format_date from ibid.utils.html import get_html_parse_tree from ibid.config import Option, DictOption from ibid.compat import defaultdict log = logging.getLogger('plugins.geography') features = {} features['distance'] = { 'description': u'Returns the distance between two places', 'categories': ('lookup', 'calculate',), } class Distance(Processor): usage = u"""distance [in ] between and place search for """ # For Mathematics, see: # http://www.mathforum.com/library/drmath/view/51711.html # http://mathworld.wolfram.com/GreatCircle.html feature = ('distance',) default_unit_names = { 'km': "kilometres", 'mi': "miles", 'nm': "nautical miles"} default_radius_values = { 'km': 6378, 'mi': 3963.1, 'nm': 3443.9} unit_names = DictOption('unit_names', 'Names of units in which to specify distances', default_unit_names) radius_values = DictOption('radius_values', 'Radius of the earth in the units in which to specify distances', default_radius_values) def get_place_data(self, place, num): return json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': num, 'username': 'ibid'}) def get_place(self, place): js = self.get_place_data(place, 1) if js['totalResultsCount'] == 0: return None info = js['geonames'][0] return {'name': "%s, %s, %s" % (info['name'], info['adminName1'], info['countryName']), 'lng': radians(info['lng']), 'lat': radians(info['lat'])} @match(r'^(?:(?:search\s+for\s+place)|(?:place\s+search\s+for)|(?:places\s+for))\s+(\S.+?)\s*$') def placesearch(self, event, place): js = self.get_place_data(place, 10) if js['totalResultsCount'] == 0: event.addresponse(u"I don't know of anywhere even remotely like '%s'", place) else: event.addresponse(u"I can find: %s", (human_join([u"%s, %s, %s" % (p['name'], p['adminName1'], p['countryName']) for p in js['geonames'][:10]], separator=u';'))) @match(r'^(?:how\s*far|distance)(?:\s+in\s+(\S+))?\s+' r'(?:(between)|from)' # Between ... and ... | from ... to ... r'\s+(\S.+?)\s+(?(2)and|to)\s+(\S.+?)\s*$') def distance(self, event, unit, ignore, src, dst): unit_names = self.unit_names if unit and unit not in self.unit_names: event.addresponse(u"I don't know the unit '%(badunit)s'. I know about: %(knownunits)s", { 'badunit': unit, 'knownunits': human_join(u"%s (%s)" % (unit, self.unit_names[unit]) for unit in self.unit_names), }) return if unit: unit_names = [unit] srcp, dstp = self.get_place(src), self.get_place(dst) if not srcp or not dstp: event.addresponse(u"I don't know of anywhere called %s", (u" or ".join("'%s'" % place[0] for place in ((src, srcp), (dst, dstp)) if not place[1]))) return dist = acos(cos(srcp['lng']) * cos(dstp['lng']) * cos(srcp['lat']) * cos(dstp['lat']) + cos(srcp['lat']) * sin(srcp['lng']) * cos(dstp['lat']) * sin(dstp['lng']) + sin(srcp['lat'])*sin(dstp['lat'])) event.addresponse(u"Approximate distance, as the bot flies, between %(srcname)s and %(dstname)s is: %(distance)s", { 'srcname': srcp['name'], 'dstname': dstp['name'], 'distance': human_join([ u"%.02f %s" % (self.radius_values[unit]*dist, self.unit_names[unit]) for unit in unit_names], conjunction=u'or'), }) features['weather'] = { 'description': u'Retrieves current weather and forecasts for cities.', 'categories': ('lookup', 'web',), } class Weather(Processor): usage = u"""weather in forecast for """ feature = ('weather',) defaults = { 'ct': 'Cape Town, South Africa', 'jhb': 'Johannesburg, South Africa', 'joburg': 'Johannesburg, South Africa', } places = DictOption('places', 'Alternate names for places', defaults) labels = ('temp', 'humidity', 'dew', 'wind', 'pressure', 'conditions', 'visibility', 'uv', 'clouds', 'ymin', 'ymax', 'ycool', 'sunrise', 'sunset', 'moonrise', 'moonset', 'moonphase', 'metar') whitespace = re.compile('\s+') class WeatherException(Exception): pass class TooManyPlacesException(WeatherException): pass def _text(self, string): if not isinstance(string, basestring): string = ''.join(string.findAll(text=True)) return self.whitespace.sub(' ', string).strip() def _get_page(self, place): if place.lower() in self.places: place = self.places[place.lower()] soup = get_html_parse_tree('http://m.wund.com/cgi-bin/findweather/getForecast?brand=mobile_metric&query=' + quote(place)) if soup.body.center and soup.body.center.b.string == 'Search not found:': raise Weather.WeatherException(u'City not found') if soup.table.tr.th and soup.table.tr.th.string == 'Place: Temperature': places = [] for td in soup.table.findAll('td'): places.append(td.find('a', href=re.compile('.*html$')).string) # Cities with more than one airport give duplicate entries. We can take the first if len([x for x in places if x == places[0]]) == len(places): url = urljoin('http://m.wund.com/cgi-bin/findweather/getForecast', soup.table.find('td').find('a', href=re.compile('.*html$'))['href']) soup = get_html_parse_tree(url) else: raise Weather.TooManyPlacesException(places) return soup def remote_weather(self, place): soup = self._get_page(place) tds = [x.table for x in soup.findAll('table') if x.table][0].findAll('td') # HACK: Some cities include a windchill row, but others don't if len(tds) == 39: del tds[3] del tds[4] values = {'place': tds[0].findAll('b')[1].string, 'time': tds[0].findAll('b')[0].string} for index, td in enumerate(tds[2::2]): values[self.labels[index]] = self._text(td) return values def remote_forecast(self, place): soup = self._get_page(place) forecasts = [] table = [table for table in soup.findAll('table') if table.findAll('td', align='left')][0] for td in table.findAll('td', align='left'): day = td.b.string forecast = u' '.join([self._text(line) for line in td.contents[2:]]) forecasts.append(u'%s: %s' % (day, self._text(forecast))) return forecasts @match(r'^weather\s+(?:(?:for|at|in)\s+)?(.+)$') def weather(self, event, place): try: values = self.remote_weather(place) event.addresponse(u'In %(place)s at %(time)s: %(temp)s; Humidity: %(humidity)s; Wind: %(wind)s; Conditions: %(conditions)s; Sunrise/set: %(sunrise)s/%(sunset)s; Moonrise/set: %(moonrise)s/%(moonset)s', values) except Weather.TooManyPlacesException, e: event.addresponse(u'Too many places match %(place)s: %(exception)s', { 'place': place, 'exception': human_join(e.args[0], separator=u';'), }) except Weather.WeatherException, e: event.addresponse(unicode(e)) @match(r'^forecast\s+(?:for\s+)?(.+)$') def forecast(self, event, place): try: event.addresponse(u', '.join(self.remote_forecast(place))) except Weather.TooManyPlacesException, e: event.addresponse(u'Too many places match %(place)s: %(exception)s', { 'place': place, 'exception': human_join(e.args[0], separator=u';'), }) except Weather.WeatherException, e: event.addresponse(unicode(e)) class TimezoneException(Exception): pass MONTH_SHORT = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') MONTH_LONG = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December') OTHER_STUFF = ('am', 'pm', 'st', 'nd', 'rd', 'th') CUSTOM_ZONES = { 'PST': 'US/Pacific', 'MST': 'US/Mountain', 'CST': 'US/Central', 'EST': 'US/Eastern', } features['timezone'] = { 'description': 'Converts times between timezones.', 'categories': ('convert',), } class TimeZone(Processor): usage = u"""when is