class SignupPage(athena.LivePage): addSlash = 1 docFactory = loaders.xmlfile(RESOURCE('templates/signup.xhtml')) def render_signup(self, ctx, data): pageURL = flat.flatten(url.URL.fromContext(ctx)) signup = Signup(pageURL) signup.setFragmentParent(self) return signup def renderHTTP(self, ctx): """ Check for a confirmation being used from the email link. If the user is confirming a login, move the password from unconfirmedPassword into password, thus enabling the account. """ req = inevow.IRequest(ctx) confirm = req.args.get('confirm', None) if confirm is not None: confirm = unicode(confirm[0]) store = theGlobal['database'] u = store.findFirst(data.User, data.User.confirmationKey == confirm) if u is not None: u.password = u.unconfirmedPassword u.unconfirmedPassword = None req.args.clear() self.docFactory = loaders.xmlfile( RESOURCE('templates/confirmed.xhtml')) else: self.docFactory = loaders.stan( ['Could not confirm that account.']) return super(SignupPage, self).renderHTTP(ctx)
class MapWidget(athena.LiveElement): implements(iwebby.IMapWidget) jsClass = u'SVGMap.MapWidget' docFactory = loaders.xmlfile(RESOURCE('elements/MapWidget')) def __init__(self, channel, chatEntry, *a, **kw): super(MapWidget, self).__init__(*a, **kw) self.channel = channel self.chatEntry = chatEntry def setMapBackgroundFromChannel(self): """ Create a new BackgroundImage widget and send it to the channel. """ image = BackgroundImage(self.channel) image.setFragmentParent(self) return self.callRemote("setMapBackground", image) def updateObscurementFromChannel(self): """ Just send a new md5key for the obscurement, which the client-side widget will use to reset the background. """ return self.callRemote("updateObscurement", self.channel.obscurement.md5) def sendCommand(self, command): return self.chatEntry.chatMessage(command) athena.expose(sendCommand)
class TextArea(util.RenderWaitLiveElement): """A scrollable widget that displays mostly lines of text.""" implements(iwebby.ITextArea) # FIXME - move to here jsClass = u"StainedGlass.TextArea" docFactory = loaders.xmlfile(RESOURCE('elements/TextArea')) widgetArgs = None def __init__(self, *a, **kw): super(TextArea, self).__init__(*a, **kw) self.messageQueue = [] def printClean(self, message): """ Send text to the widget if it is ready; if not, queue text for later. """ message = util.flattenMessageString(message) return self.callRemote('appendTo', message) def setInitialArguments(self, *a, **kw): # FIXME - raped from tabs.py assert len(kw) == 0, "Cannot pass keyword arguments to a Widget" self.widgetArgs = a def getInitialArguments(self): # FIXME - raped from tabs.py args = () if self.widgetArgs is not None: args = self.widgetArgs return args
class TabsElement(athena.LiveElement): docFactory = loaders.xmlfile(RESOURCE('elements/Tabs')) jsClass = u"Tabby.TabsElement" widgetArgs = EMPTY def addTab(self, id, label): return self.callRemote('addTab', id, label) def setTabBody(self, id, content): return self.callRemote('setTabBody', id, content) def removeTab(self, id): return self.callRemote('removeTab', id) def addInitialTab(self, id, label, content): """ Specify a tab that will be sent in the initial arguments. Has no effect after the first time this widget gets rendered. """ assert getattr(self, '_athenaID', None) is None, ( "Cannot call this after the widget has rendered.") if self.widgetArgs is EMPTY: self.widgetArgs = [] self.widgetArgs.append((id, label, content)) def getInitialArguments(self): return self.widgetArgs
class FileChooser(athena.LiveElement): implements(IFileObserver) jsClass = u'WebbyVellum.FileChooser' docFactory = loaders.xmlfile(RESOURCE('elements/FileChooser')) def __init__(self, user, *a, **kw): super(FileChooser, self).__init__(*a, **kw) self.user = user def chooser(self, req, tag): # get file notifications from the user. set this as early as possible # in the render (which is here) self.user.addObserver(self) self.page.notifyOnDisconnect().addBoth( lambda reason: self.user.removeObserver(self)) return tag[self._getIconsFromDatabase()] page.renderer(chooser) def _getIconsFromDatabase(self): """ @return a list of the choosericons, pre-rendering. """ db = theGlobal['database'] _fileitems = db.query(data.FileMeta, data.FileMeta.user == self.user, sort=data.FileMeta.filename.ascending) ret = [self._newIconFromItem(fi) for fi in _fileitems] return ret def _newIconFromItem(self, fileitem): ch = ChooserIcon(self.user, fileitem) ch.setFragmentParent(self) return ch def fileAdded(self, fileitem): """ Construct a new icon and send it to the browser """ icon = self._newIconFromItem(fileitem) return self.callRemote('fileAdded', icon) def fileRemoved(self, fileitem): """ Get a reference to the LiveElement for that item, and make a .remove call on it directly """ TODO return eelf.fileItems[fileitem].remove(FOO) def fileModified(self, fileitem): """ Get a reference to the LiveElement for that item, and make a .modify call in it. TODO Which parts need to be sent? """ TODO return self.fileItems[fileitem].modify(FOO)
class AccountManager(athena.LiveElement): """ UI element that handles changing of the nick and processing a login to the IRC server. """ jsClass = u"WebbyVellum.AccountManager" docFactory = loaders.xmlfile(RESOURCE('elements/AccountManager')) implements(iwebby.IChatAccountManager) def __init__(self, accountManager, conversationTabs, user, *a, **kw): super(AccountManager, self).__init__(*a, **kw) self.accountManager = accountManager self.conversationTabs = conversationTabs self.user = user def getInitialArguments(self): nick = self.user.nick channels = self.user.recentChannels if nick and channels: # when both nick and channels are already set, log the irc user # straight in. self.onLogOnSubmit(nick, channels) # set autoHide = True on the widget because login will be # automatic return (nick, channels, True) return (nick, channels) def onLogOnSubmit(self, nick, channels): """ Process by looking up the password for that nick and starting a login process on the IRC protocol. """ host = 'localhost'.encode('utf8') password = self.user.password.encode('utf8') username = nick.encode('utf8') # SET the permanent nick to the nick we were provided, trust it. # We can trust the nick because use of AccountManager # implies that the user is *already* authenticated, through # the web. Users visiting using a regular IRC client will # naturally have to supply their passwords the normal way. self.user.nick = unicode(nick) channels = channels.encode('utf8') d = self.accountManager.doConnection(host, username, password, channels) def _gotAccount(acct): # set up disconnection callback for browser close etc. def logOff(_ignored, acct): self.accountManager.disconnectAsNeeded(acct) self.page.notifyOnDisconnect().addBoth(logOff, acct) return u'connected %s@%s and joined %s' % (username, host, channels) d.addCallback(_gotAccount) return d athena.expose(onLogOnSubmit)
class TopicBar(util.RenderWaitLiveElement): """ Text widget that sits above the channel window in a tab and displays the current channel topic. """ implements(iwebby.ITopicBar) docFactory = loaders.xmlfile(RESOURCE('elements/TopicBar')) jsClass = u'WebbyVellum.TopicBar' def setTopic(self, topic): topic = unicode(topic) return self.callRemote('setTopic', topic)
class Enclosure(athena.LiveElement): """ @ivar windowTitle: A title that will be shown at the top of the widget @ivar userClass: additional text to add at the end of the class=".." attribute @ivar decorated: if False, no decorations (border, titlebar, etc.) """ jsClass = u"StainedGlass.Enclosure" docFactory = loaders.xmlfile(RESOURCE('elements/Enclosure')) def __init__(self, windowTitle='~', userClass='', decorated=True, *a, **kw): super(Enclosure, self).__init__(*a, **kw) self.windowTitleStan = windowTitle self.userClassStan = userClass self.children = [] self.decorated = decorated def enclosedRegion(self, request, tag): return tag[self.children] athena.renderer(enclosedRegion) def __getitem__(self, children): self.children = children return self def extraClasses(self, request, tag): tag.fillSlots('userClass', self.userClassStan) if self.decorated: tag.fillSlots('decorationClass', 'decorated') else: tag.fillSlots('decorationClass', '') return tag athena.renderer(extraClasses) def windowTitle(self, request, tag): tag.fillSlots('windowTitle', self.windowTitleStan) return tag athena.renderer(windowTitle) def decorations(self, req, tag): if self.decorated: return tag return [] athena.renderer(decorations)
class Signup(athena.LiveElement): docFactory = loaders.xmlfile(RESOURCE('elements/Signup')) jsClass = u'Signup.Signup' def __init__(self, pageURL, *a, **kw): super(Signup, self).__init__(*a, **kw) self.pageURL = pageURL def processSignup(self, email, password): key = util.label() store = theGlobal['database'] u = store.findFirst(data.User, data.User.email == email) if u is None: u = data.User(store=store, email=email, password=None, unconfirmedPassword=password, confirmationKey=key) else: u.unconfirmedPassword = password u.confirmationKey = key link = self.pageURL + '?confirm=%s' % (key, ) d = sendEmail( email, "Confirm Vellum Signup", ## {{{ """ You signed up to use Vellum, I think. Confirm your signup on the Vellum website by clicking on the following link: %s """ % (link, )) ## }}} d.addCallback(lambda response: unicode(response)) def _didntSendEmail(failure): """Remove the new user object if we can't even send the email""" u.deleteFromStore() return failure d.addErrback(_didntSendEmail) return d athena.expose(processSignup)
class ChannelLayout(page.Element): docFactory = loaders.xmlfile(RESOURCE('elements/ChannelLayout')) def __init__(self, controlBar, topicBar, mapDiv, channelDiv, chatEntry): self.controlBar = controlBar self.topicBar = topicBar self.mapDiv = mapDiv self.channelDiv = channelDiv self.chatEntry = chatEntry def layout(self, req, tag): tag.fillSlots('controlBar', self.controlBar) tag.fillSlots('topicBar', self.topicBar) tag.fillSlots('mapDiv', self.mapDiv) tag.fillSlots('channelDiv', self.channelDiv) tag.fillSlots('chatEntry', self.chatEntry) return tag page.renderer(layout)
class IRCPage(athena.LivePage): """ Page container for the IRC UI and the maps UI """ addSlash = True docFactory = loaders.xmlfile(RESOURCE('templates/webby.xhtml')) def renderHTTP(self, ctx): """Set XHTML as the mime type so SVG can be rendered.""" req = inevow.IRequest(ctx) req.setHeader('content-type', 'application/xhtml+xml') return super(IRCPage, self).renderHTTP(ctx) def __init__(self, *a, **kw): super(IRCPage, self).__init__(*a, **kw) self.chatui = minchat.MinChat() def render_chat(self, ctx, _): accountManager = minchat.MinAccountManager(self.chatui) ss = inevow.ISession(ctx) irc = IRCContainer(accountManager, ss.user) irc.setFragmentParent(self) self.chatui.initUI(irc) return ctx.tag[irc] def render_gmtools(self, ctx, _): ss = inevow.ISession(ctx) gmt = gmtools.GMTools(ss.user) enc = stainedglass.Enclosure(windowTitle="GM Tools", userClass="gmtools draggable") enc.setFragmentParent(self) gmt.setFragmentParent(enc) return ctx.tag[enc[gmt]] def render_topLinks(self, ctx, _): ss = inevow.ISession(ctx) ctx.tag.fillSlots('email', ss.user.email) ctx.tag.fillSlots('logoutHref', url.root.child(guard.LOGOUT_AVATAR)) ctx.tag.fillSlots('adminLink', T.a['Admin (TODO)']) return ctx.tag
class BackgroundImage(athena.LiveElement): ## jsClass = u'SVGMap.BackgroundImage' docFactory = loaders.xmlfile(RESOURCE('elements/BackgroundImage')) def __init__(self, channel, *a, **kw): super(BackgroundImage, self).__init__(*a, **kw) self.channel = channel def imageLiveElement(self, req, tag): ch = self.channel href = u'/files/%s' % (ch.background.md5, ) obscurementHref = u'/files/%s' % (ch.obscurement.md5, ) tag.fillSlots('width', ch.background.width) tag.fillSlots('height', ch.background.height) tag.fillSlots('href', href) tag.fillSlots('obscurementHref', obscurementHref) return tag(render=T.directive("liveElement")) athena.renderer(imageLiveElement)
class ChooserIcon(athena.LiveElement): docFactory = loaders.xmlfile(RESOURCE('elements/ChooserIcon')) jsClass = u'WebbyVellum.ChooserIcon' def __init__(self, user, fileitem, *a, **kw): super(ChooserIcon, self).__init__(*a, **kw) self.fileitem = fileitem self.user = user def chooserIcon(self, req, tag): fi = self.fileitem tag.fillSlots('filename', fi.filename) if fi.mimeType.startswith(u'image/') and fi.thumbnail is not None: tag.fillSlots('icon', '/files/%s/thumb' % (fi.md5, )) else: tag.fillSlots('icon', '/static/%s' % (iconForMimeType(fi.mimeType), )) return tag page.renderer(chooserIcon)
class NameSelect(util.RenderWaitLiveElement): """ <select> box that contains the list of names for a group conversation. """ implements(iwebby.INameSelect) jsClass = u'WebbyVellum.NameSelect' docFactory = loaders.xmlfile(RESOURCE('elements/NameSelect')) def addName(self, name, flags): # TODO - parse flags name = unicode(name) return self.callRemote('addName', name, None) def removeName(self, name): name = unicode(name) return self.callRemote('removeName', name) def setNames(self, names): # TODO - flags? return self.callRemote('setNames', map(unicode, names))
class LoginPage(StaticRoot): """Page which asks for username/password.""" addSlash = True docFactory = loaders.xmlfile(RESOURCE('templates/login.xhtml')) def child_game(self, ctx,): """Redirect to the login page when you attempt to return to /game""" return url.root.child('') def render_form(self, ctx, data): req = inevow.IRequest(ctx) if 'login-failure' in req.args: ctx.tag.fillSlots('loginStatus', ctx.tag.onePattern('unauthorized')) else: ctx.tag.fillSlots('loginStatus', []) ctx.tag.fillSlots('action', guard.LOGIN_AVATAR) return ctx.tag def logout(self): print "Bye"
def renderHTTP(self, ctx): """ Check for a confirmation being used from the email link. If the user is confirming a login, move the password from unconfirmedPassword into password, thus enabling the account. """ req = inevow.IRequest(ctx) confirm = req.args.get('confirm', None) if confirm is not None: confirm = unicode(confirm[0]) store = theGlobal['database'] u = store.findFirst(data.User, data.User.confirmationKey == confirm) if u is not None: u.password = u.unconfirmedPassword u.unconfirmedPassword = None req.args.clear() self.docFactory = loaders.xmlfile( RESOURCE('templates/confirmed.xhtml')) else: self.docFactory = loaders.stan( ['Could not confirm that account.']) return super(SignupPage, self).renderHTTP(ctx)
class UploadPage(formal.ResourceMixin, rend.Page): """ Perform file uploads which will be stored in the Axiom store. """ addSlash = True docFactory = loaders.xmlfile(RESOURCE('templates/upload.xhtml')) def __init__(self, user, *a, **kw): formal.ResourceMixin.__init__(self, *a, **kw) rend.Page.__init__(self, *a, **kw) self.user = user def form_upload(self, ctx): f = formal.Form() f.addField('file', formal.File()) f.addAction(self.saveFile, name="submit", label="Upload File") return f def saveFile(self, ctx, form, data): """Receive the file and drop it into the Axiom box.""" db = theGlobal['database'] filename, filedata = data['file'] # when the file was not successfully uploaded, filename==u'' if len(filename) > 0: readdata = filedata.read() m = unicode(md5.md5(readdata).hexdigest()) # make sure that a particular file can only be uploaded once if db.findFirst(FileMeta, FileMeta.md5 == m) is None: # so we really did get a file upload def txn(): mimeType = unicode(mimetypes.guess_type(filename)[0]) newFileData = FileData(store=db, data=readdata) newfile = FileMeta( store=db, filename=filename, data=newFileData, md5=m, mimeType=mimeType, user=self.user, ) # now thumbnail it and store image metas if mimeType.startswith('image/'): # PIL can't thumbnail every identifiable kind of # image, so just punt if it fails to update. try: filedata.seek(0) original = Image.open(filedata) thumb = original.copy() thumb.thumbnail((48, 48), Image.ANTIALIAS) _tempfile = StringIO() thumb.save(_tempfile, 'PNG', optimize=True) _tempfile.seek(0) newThumbData = FileData(store=db, data=_tempfile.read()) newfile.thumbnail = newThumbData newfile.width, newfile.height = original.size except IOError: pass # notify the user, so the user can notify observers that # the file list has changed. self.user.fileAdded(newfile) db.transact(txn) return UploadDone()
def child_static(self, ctx): return static.File(RESOURCE('static'))
import os from nevow import athena from webby import RESOURCE webbyPkg = athena.AutoJSPackage(RESOURCE('static'))
class ControlBar(page.Element): docFactory = loaders.xmlfile(RESOURCE('elements/ControlBar'))
from twisted.python import log from zope.interface import implements from nevow import loaders, athena, rend, inevow, url, tags as T, flat, page from webby import tabs, util, theGlobal, data, RESOURCE from webby.data import FileMeta, FileData from webby.iwebby import IFileObserver import formal # load our own mime-types, because the set python comes with by default is # *pathetic* mimetypes.init([ RESOURCE('mime.types'), ]) # map MIME types to image filename. This is a list because the # matches are meant to be done in order. Thus, the most generic # icon is the last one. mimeIcons = [ ('image/.*', 'image-x-generic.png'), # {{{ ('audio/.*', 'audio-x-generic.png'), ('video/.*', 'video-x-generic.png'), ('text/x-.*sh', 'application-x-executable.png'), ('text/.*', 'text-x-generic.png'), ('application/java-archive', 'application-x-executable.png'), ('application/x-msdos-program', 'application-x-executable.png'), ('application/x-msi', 'application-x-executable.png'), ('application/zip', 'package-x-generic.png'),
class UploadDone(rend.Page): docFactory = loaders.xmlfile(RESOURCE('templates/uploaddone.xhtml'))
class ChatEntry(athena.LiveElement): docFactory = loaders.xmlfile(RESOURCE('elements/ChatEntry')) jsClass = u"WebbyVellum.ChatEntry" def __init__(self, conversation, *a, **kw): super(ChatEntry, self).__init__(*a, **kw) self.conversation = conversation def chatMessage(self, message): conv = self.conversation parsed = parseirc.line.parseString(message) if parsed.command: def irccmdFallback(message, conv): strCommand = parsed.commandWord.encode('utf8').upper() message = '%s %s' % (strCommand, message) return self.irccmd_raw(message, conv) m = getattr(self, 'irccmd_%s' % (parsed.commandWord,), irccmdFallback) m(parsed.commandArgs.encode('utf8'), conv) else: self.say(parsed.nonCommand[0].encode('utf8'), conv) return u'ok' athena.expose(chatMessage) def say(self, message, conv): return conv.sendText(message) def irccmd_me(self, args, conv): return conv.sendText(args, metadata={'style':'emote'}) def irccmd_join(self, args, conv): groups = args.split() ## acct = conv.group.account # We're using this way to get the account because I can't figure out a # way to make it so all conversations have access to the account. I # don't know if this will work. FIXME acct = self.page.chatui.onlineClients[0].account if groups: args = args[len(groups[0])-1:].lstrip() groups = groups[0].split(',') acct.joinGroups(groups) irccmd_j = irccmd_join def irccmd_topic(self, args, conv): client = self.page.chatui.onlineClients[0] channel = None # see if the user is trying to see/set the topic for some other # channel. This applies if the first word begins with a hash #. if args.strip() != '': firstArg = args.split()[0] if firstArg[0] == '#': # remove the channel. args = args[len(firstArg):] args = args.lstrip() channel = firstArg if channel is None: if hasattr(conv, 'group'): channel = conv.group.name else: return conv.sendText("Cannot set or view topic of SERVER tab") channel = channel.lstrip('#') if args.strip() == '': args = None client.topic(channel, args) def irccmd_part(self, args, conv): groups = args.split() ## acct = conv.group.account # We're using this way to get the account because I can't figure out a # way to make it so all conversations have access to the account. I # don't know if this will work. acct = self.page.chatui.onlineClients[0].account if groups: args = args[len(groups[0])-1:].lstrip() groups = groups[0].split(',') else: if hasattr(conv, 'group'): groups.append(conv.group.name.lstrip('#')) else: return conv.sendText("Cannot /part from SERVER tab") # TODO: Find out how to support parting messages acct.leaveGroups(groups) irccmd_leave = irccmd_part def irccmd_raw(self, args, conv): ## acct = conv.group.account # We're using this way to get the account because I can't figure out a # way to make it so all conversations have access to the account. I # don't know if this will work. acct = self.page.chatui.onlineClients[0].account return acct.client.sendLine(args) def irccmd_notice(self, args, conv): client = self.page.chatui.onlineClients[0] recipient, rest = args.split(None, 1) r = client.notice(recipient, rest) iwebby.ITextArea(conv).printClean(u'>%s< %s' % (recipient, rest)) return r def irccmd_query(self, args, conv): try: personName = args.split()[0] mesg = args[len(personName):].lstrip() chatui = self.page.chatui client = chatui.onlineClients[0] newConv = chatui.getConversation(chatui.getPerson(personName, client)) newConv.show() if mesg: newConv.sendText(mesg) except: conv.sendText("Problems with /query, bailing out.")