| 1 | from epsilon.extime import Time |
|---|
| 2 | |
|---|
| 3 | from twisted.cred.portal import IRealm |
|---|
| 4 | from twisted.python.filepath import FilePath |
|---|
| 5 | |
|---|
| 6 | from axiom.scripts import axiomatic |
|---|
| 7 | from axiom.dependency import installOn, uninstallFrom |
|---|
| 8 | from axiom.attributes import AND |
|---|
| 9 | |
|---|
| 10 | from xmantissa import publicweb |
|---|
| 11 | |
|---|
| 12 | from eridanus import plugin, util |
|---|
| 13 | from eridanus.bot import IRCBotService, IRCBotFactoryFactory, IRCBotConfig |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | class ConfigureService(axiomatic.AxiomaticSubCommand): |
|---|
| 17 | longdesc = 'Configure an Eridanus service' |
|---|
| 18 | |
|---|
| 19 | optParameters = [ |
|---|
| 20 | ('id', None, None, 'Service identifier'), |
|---|
| 21 | ('name', None, None, 'Network name'), |
|---|
| 22 | ('host', 'h', None, 'Service hostname'), |
|---|
| 23 | ('port', 'p', 6667, 'Service port number'), |
|---|
| 24 | ('nick', 'n', None, 'Bot nickname'), |
|---|
| 25 | ('channels', 'c', None, 'Channels to join'), |
|---|
| 26 | ('ignores', 'i', None, 'Nicknames to ignore'), |
|---|
| 27 | ] |
|---|
| 28 | |
|---|
| 29 | def getStore(self): |
|---|
| 30 | return self.parent.getStore() |
|---|
| 31 | |
|---|
| 32 | def postOptions(self): |
|---|
| 33 | store = self.getStore() |
|---|
| 34 | |
|---|
| 35 | svc = store.findUnique(IRCBotService, IRCBotService.serviceID == self['id']) |
|---|
| 36 | if svc.config is None: |
|---|
| 37 | svc.config = config = IRCBotConfig(store=store) |
|---|
| 38 | else: |
|---|
| 39 | config = svc.config |
|---|
| 40 | |
|---|
| 41 | if self['name']: |
|---|
| 42 | config.name = self.decodeCommandLine(self['name']) |
|---|
| 43 | if self['host']: |
|---|
| 44 | config.hostname = self['host'] |
|---|
| 45 | if self['port']: |
|---|
| 46 | config.portNumber = int(self['port']) |
|---|
| 47 | if self['nick']: |
|---|
| 48 | config.nickname = self.decodeCommandLine(self['nick']) |
|---|
| 49 | if self['channels']: |
|---|
| 50 | config.channels = self.decodeCommandLine(self['channels']).split(u',') |
|---|
| 51 | if self['ignores']: |
|---|
| 52 | config.ignores = self.decodeCommandLine(self['ignores']).split(u',') |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | def createService(siteStore, serviceID): |
|---|
| 56 | fact = siteStore.findOrCreate(IRCBotFactoryFactory) |
|---|
| 57 | svc = siteStore.findOrCreate(IRCBotService, |
|---|
| 58 | serviceID=serviceID, |
|---|
| 59 | factory=fact) |
|---|
| 60 | try: |
|---|
| 61 | installOn(svc, siteStore) |
|---|
| 62 | except: |
|---|
| 63 | pass |
|---|
| 64 | return svc |
|---|
| 65 | |
|---|
| 66 | |
|---|
| 67 | class CreateService(axiomatic.AxiomaticSubCommand): |
|---|
| 68 | longdesc = 'Create a new Eridanus service' |
|---|
| 69 | |
|---|
| 70 | optParameters = [ |
|---|
| 71 | ('id', None, None, 'Service identifier'), |
|---|
| 72 | ] |
|---|
| 73 | |
|---|
| 74 | def getStore(self): |
|---|
| 75 | return self.parent.getStore() |
|---|
| 76 | |
|---|
| 77 | def postOptions(self): |
|---|
| 78 | store = self.getStore() |
|---|
| 79 | createService(store, self['id']) |
|---|
| 80 | |
|---|
| 81 | |
|---|
| 82 | class RemoveService(axiomatic.AxiomaticSubCommand): |
|---|
| 83 | longdesc = 'Remove an existing Eridanus service' |
|---|
| 84 | |
|---|
| 85 | optParameters = [ |
|---|
| 86 | ('id', None, None, 'Service identifier'), |
|---|
| 87 | ] |
|---|
| 88 | |
|---|
| 89 | def getStore(self): |
|---|
| 90 | return self.parent.getStore() |
|---|
| 91 | |
|---|
| 92 | def postOptions(self): |
|---|
| 93 | store = self.getStore() |
|---|
| 94 | |
|---|
| 95 | fact = store.findUnique(IRCBotFactoryFactory) |
|---|
| 96 | svc = store.findUnique(IRCBotService, |
|---|
| 97 | AND(IRCBotService.serviceID == self['id'], |
|---|
| 98 | IRCBotService.factory == fact)); |
|---|
| 99 | |
|---|
| 100 | uninstallFrom(svc, store) |
|---|
| 101 | |
|---|
| 102 | |
|---|
| 103 | class ListServices(axiomatic.AxiomaticSubCommand): |
|---|
| 104 | longdesc = 'List available Eridanus services' |
|---|
| 105 | |
|---|
| 106 | def getStore(self): |
|---|
| 107 | return self.parent.getStore() |
|---|
| 108 | |
|---|
| 109 | def postOptions(self): |
|---|
| 110 | store = self.getStore() |
|---|
| 111 | print '\n'.join(store.query(IRCBotService).getColumn('serviceID')) |
|---|
| 112 | |
|---|
| 113 | |
|---|
| 114 | class ManageServices(axiomatic.AxiomaticSubCommand): |
|---|
| 115 | longdesc = 'Manage Eridanus services' |
|---|
| 116 | |
|---|
| 117 | subCommands = [ |
|---|
| 118 | ('create', None, CreateService, 'Create a new service'), |
|---|
| 119 | ('remove', None, RemoveService, 'Remove an existing service'), |
|---|
| 120 | ('config', None, ConfigureService, 'Set service configuration data'), |
|---|
| 121 | ('list', None, ListServices, 'List available services'), |
|---|
| 122 | ] |
|---|
| 123 | |
|---|
| 124 | def getStore(self): |
|---|
| 125 | return self.parent.getStore() |
|---|
| 126 | |
|---|
| 127 | def getAppStore(self): |
|---|
| 128 | return self.parent.getAppStore() |
|---|
| 129 | |
|---|
| 130 | |
|---|
| 131 | # XXX: this shouldn't be anywhere near here |
|---|
| 132 | from eridanusstd import linkdb |
|---|
| 133 | from eridanusstd.linkdb import LinkManager, LinkEntry, LinkEntryComment, LinkEntryMetadata |
|---|
| 134 | class ImportExportFile(object): |
|---|
| 135 | encoding = 'utf-8' |
|---|
| 136 | |
|---|
| 137 | def __init__(self, fd, appStore): |
|---|
| 138 | self.fd = fd |
|---|
| 139 | self.appStore = appStore |
|---|
| 140 | self.eof = False |
|---|
| 141 | self.count = 0 |
|---|
| 142 | self._typeConverters = {'bytes': (self.writeline, self.readline), |
|---|
| 143 | 'text': (self.writeText, self.readText), |
|---|
| 144 | 'timestamp': (self.writeTimestamp, self.readTimestamp), |
|---|
| 145 | 'integer': (self.writeInteger, self.readInteger), |
|---|
| 146 | 'textlist': (self.writeTextList, self.readTextList), |
|---|
| 147 | 'boolean': (self.writeBoolean, self.readBoolean)} |
|---|
| 148 | |
|---|
| 149 | def write(self, s): |
|---|
| 150 | self.fd.write(s) |
|---|
| 151 | |
|---|
| 152 | def writeline(self, s): |
|---|
| 153 | self.write(s + '\n') |
|---|
| 154 | |
|---|
| 155 | def readline(self): |
|---|
| 156 | self.count += 1 |
|---|
| 157 | line = self.fd.readline() |
|---|
| 158 | if not line: |
|---|
| 159 | self.eof = True |
|---|
| 160 | |
|---|
| 161 | return line.rstrip() |
|---|
| 162 | |
|---|
| 163 | def writeInteger(self, value): |
|---|
| 164 | self.writeline(str(value)) |
|---|
| 165 | |
|---|
| 166 | def readInteger(self): |
|---|
| 167 | return int(self.readline()) |
|---|
| 168 | |
|---|
| 169 | def writeTimestamp(self, value): |
|---|
| 170 | self.writeline(str(value.asPOSIXTimestamp())) |
|---|
| 171 | |
|---|
| 172 | def readTimestamp(self): |
|---|
| 173 | return Time.fromPOSIXTimestamp(float(self.readline())) |
|---|
| 174 | |
|---|
| 175 | def writeText(self, value): |
|---|
| 176 | if value is None: |
|---|
| 177 | data = '' |
|---|
| 178 | else: |
|---|
| 179 | data = value.encode(self.encoding) |
|---|
| 180 | self.writeline(data) |
|---|
| 181 | |
|---|
| 182 | def readText(self): |
|---|
| 183 | data = self.readline() |
|---|
| 184 | if not data: |
|---|
| 185 | return None |
|---|
| 186 | return data.decode(self.encoding) |
|---|
| 187 | |
|---|
| 188 | def writeBoolean(self, value): |
|---|
| 189 | return self.writeline(str(int(value))) |
|---|
| 190 | |
|---|
| 191 | def readBoolean(self): |
|---|
| 192 | return bool(int(self.readline())) |
|---|
| 193 | |
|---|
| 194 | def writeTextList(self, value): |
|---|
| 195 | data = '\1'.join(t.encode(self.encoding) for t in value) |
|---|
| 196 | self.writeline(data) |
|---|
| 197 | |
|---|
| 198 | def readTextList(self): |
|---|
| 199 | return [t.decode(self.encoding) for t in self.readline().split('\1')] |
|---|
| 200 | |
|---|
| 201 | def writeItem(self, item, attrs): |
|---|
| 202 | for attrName in attrs: |
|---|
| 203 | typeName = getattr(type(item), attrName).__class__.__name__ |
|---|
| 204 | writer = self._typeConverters[typeName][0] |
|---|
| 205 | writer(getattr(item, attrName)) |
|---|
| 206 | |
|---|
| 207 | def readItem(self, itemType, attrs): |
|---|
| 208 | def _readAttributes(): |
|---|
| 209 | for attrName in attrs: |
|---|
| 210 | typeName = getattr(itemType, attrName).__class__.__name__ |
|---|
| 211 | reader = self._typeConverters[typeName][1] |
|---|
| 212 | yield attrName, reader() |
|---|
| 213 | |
|---|
| 214 | return dict(_readAttributes()) |
|---|
| 215 | |
|---|
| 216 | serviceAttrs = ['serviceID'] |
|---|
| 217 | |
|---|
| 218 | def writeService(self, service): |
|---|
| 219 | self.writeline('service') |
|---|
| 220 | self.writeItem(service, self.serviceAttrs) |
|---|
| 221 | |
|---|
| 222 | self.writeConfig(service.config) |
|---|
| 223 | |
|---|
| 224 | for manager in linkdb.getAllLinkManagers(self.appStore, service.serviceID): |
|---|
| 225 | self.writeEntryManager(manager) |
|---|
| 226 | |
|---|
| 227 | def readService(self): |
|---|
| 228 | return self.readItem(IRCBotService, self.serviceAttrs) |
|---|
| 229 | |
|---|
| 230 | configAttrs = ['name', 'hostname', 'portNumber', 'nickname', 'channels', 'ignores'] |
|---|
| 231 | |
|---|
| 232 | def writeConfig(self, config): |
|---|
| 233 | self.writeline('config') |
|---|
| 234 | self.writeItem(config, self.configAttrs) |
|---|
| 235 | |
|---|
| 236 | def readConfig(self): |
|---|
| 237 | return self.readItem(IRCBotConfig, self.configAttrs) |
|---|
| 238 | |
|---|
| 239 | entryManagerAttrs = ['channel', 'lastEid'] |
|---|
| 240 | |
|---|
| 241 | def writeEntryManager(self, entryManager): |
|---|
| 242 | self.writeline('entrymanager') |
|---|
| 243 | self.writeItem(entryManager, self.entryManagerAttrs) |
|---|
| 244 | |
|---|
| 245 | for entry in entryManager.getEntries(discarded=None, deleted=None): |
|---|
| 246 | self.writeEntry(entry) |
|---|
| 247 | |
|---|
| 248 | def readEntryManager(self): |
|---|
| 249 | return self.readItem(LinkManager, self.entryManagerAttrs) |
|---|
| 250 | |
|---|
| 251 | entryAttrs = ['eid', 'created', 'modified', 'channel', 'nick', 'url', 'title', 'occurences', 'isDiscarded', 'isDeleted'] |
|---|
| 252 | |
|---|
| 253 | def writeEntry(self, entry): |
|---|
| 254 | self.writeline('entry') |
|---|
| 255 | self.writeItem(entry, self.entryAttrs) |
|---|
| 256 | |
|---|
| 257 | for comment in entry.getComments(): |
|---|
| 258 | self.writeComment(comment) |
|---|
| 259 | |
|---|
| 260 | for metadata in entry._getMetadata(): |
|---|
| 261 | self.writeMetadata(metadata) |
|---|
| 262 | |
|---|
| 263 | def readEntry(self): |
|---|
| 264 | return self.readItem(LinkEntry, self.entryAttrs) |
|---|
| 265 | |
|---|
| 266 | commentAttrs = ['created', 'nick', 'comment', 'initial'] |
|---|
| 267 | |
|---|
| 268 | def writeComment(self, comment): |
|---|
| 269 | self.writeline('comment') |
|---|
| 270 | self.writeItem(comment, self.commentAttrs) |
|---|
| 271 | |
|---|
| 272 | def readComment(self): |
|---|
| 273 | return self.readItem(LinkEntryComment, self.commentAttrs) |
|---|
| 274 | |
|---|
| 275 | metadataAttrs = ['kind', 'data'] |
|---|
| 276 | |
|---|
| 277 | def writeMetadata(self, metadata): |
|---|
| 278 | self.writeline('metadata') |
|---|
| 279 | self.writeItem(metadata, self.metadataAttrs) |
|---|
| 280 | |
|---|
| 281 | def readMetadata(self): |
|---|
| 282 | return self.readItem(LinkEntryMetadata, self.metadataAttrs) |
|---|
| 283 | |
|---|
| 284 | |
|---|
| 285 | # XXX: this shouldn't be anywhere near here |
|---|
| 286 | class ExportEntries(axiomatic.AxiomaticSubCommand): |
|---|
| 287 | longdesc = 'Export linkdb entries to disk' |
|---|
| 288 | |
|---|
| 289 | optParameters = [ |
|---|
| 290 | ('path', 'p', None, 'Path to output export data to'), |
|---|
| 291 | ] |
|---|
| 292 | |
|---|
| 293 | def getStore(self): |
|---|
| 294 | return self.parent.getStore() |
|---|
| 295 | |
|---|
| 296 | def getAppStore(self): |
|---|
| 297 | return self.parent.getAppStore() |
|---|
| 298 | |
|---|
| 299 | def postOptions(self): |
|---|
| 300 | appStore = self.getAppStore() |
|---|
| 301 | store = self.getStore() |
|---|
| 302 | |
|---|
| 303 | outroot = FilePath(self['path']) |
|---|
| 304 | if not outroot.exists(): |
|---|
| 305 | outroot.makedirs() |
|---|
| 306 | |
|---|
| 307 | for i, service in enumerate(store.query(IRCBotService)): |
|---|
| 308 | print 'Processing service %r...' % (service.serviceID,) |
|---|
| 309 | |
|---|
| 310 | fd = outroot.child(str(i)).open('wb') |
|---|
| 311 | ief = ImportExportFile(fd, appStore) |
|---|
| 312 | ief.writeService(service) |
|---|
| 313 | |
|---|
| 314 | |
|---|
| 315 | # XXX: this shouldn't be anywhere near here |
|---|
| 316 | class ImportEntries(axiomatic.AxiomaticSubCommand): |
|---|
| 317 | longdesc = 'Import linkdb entries from an export' |
|---|
| 318 | |
|---|
| 319 | optFlags = [ |
|---|
| 320 | ('clear', None, 'Remove existing entries before performing the import'), |
|---|
| 321 | ] |
|---|
| 322 | |
|---|
| 323 | optParameters = [ |
|---|
| 324 | ('path', 'p', None, 'Path to read export data from'), |
|---|
| 325 | ] |
|---|
| 326 | |
|---|
| 327 | def getStore(self): |
|---|
| 328 | return self.parent.getStore() |
|---|
| 329 | |
|---|
| 330 | def getAppStore(self): |
|---|
| 331 | return self.parent.getAppStore() |
|---|
| 332 | |
|---|
| 333 | def postOptions(self): |
|---|
| 334 | appStore = self.getAppStore() |
|---|
| 335 | siteStore = self.getStore() |
|---|
| 336 | |
|---|
| 337 | inroot = FilePath(self['path']) |
|---|
| 338 | |
|---|
| 339 | availableModes = ['service', 'config', 'entrymanager', 'entry', 'comment', 'metadata'] |
|---|
| 340 | |
|---|
| 341 | if self['clear']: |
|---|
| 342 | appStore.query(LinkEntryComment).deleteFromStore() |
|---|
| 343 | appStore.query(LinkEntryMetadata).deleteFromStore() |
|---|
| 344 | appStore.query(LinkEntry).deleteFromStore() |
|---|
| 345 | appStore.query(LinkManager).deleteFromStore() |
|---|
| 346 | |
|---|
| 347 | mode = None |
|---|
| 348 | service = None |
|---|
| 349 | config = None |
|---|
| 350 | entryManager = None |
|---|
| 351 | entry = None |
|---|
| 352 | |
|---|
| 353 | for fp in inroot.globChildren('*'): |
|---|
| 354 | fd = fp.open() |
|---|
| 355 | ief = ImportExportFile(fd, appStore) |
|---|
| 356 | |
|---|
| 357 | while True: |
|---|
| 358 | line = ief.readline() |
|---|
| 359 | if ief.eof: |
|---|
| 360 | break |
|---|
| 361 | |
|---|
| 362 | if line in availableModes: |
|---|
| 363 | mode = line |
|---|
| 364 | |
|---|
| 365 | if mode == 'service': |
|---|
| 366 | kw = ief.readService() |
|---|
| 367 | service = createService(siteStore, **kw) |
|---|
| 368 | elif mode == 'config': |
|---|
| 369 | kw = ief.readConfig() |
|---|
| 370 | service.config = config = IRCBotConfig(store=siteStore, **kw) |
|---|
| 371 | elif mode == 'entrymanager': |
|---|
| 372 | assert service is not None |
|---|
| 373 | kw = ief.readEntryManager() |
|---|
| 374 | print 'Creating entry manager for %(channel)s...' % kw |
|---|
| 375 | entryManager = LinkManager(store=appStore, serviceID=service.serviceID, **kw) |
|---|
| 376 | if self['clear']: |
|---|
| 377 | entryManager.searchIndexer.reset() |
|---|
| 378 | elif mode == 'entry': |
|---|
| 379 | assert entryManager is not None |
|---|
| 380 | kw = ief.readEntry() |
|---|
| 381 | #print 'Creating entry #%(eid)s for %(channel)s...' % kw |
|---|
| 382 | entry = LinkEntry(store=appStore, **kw) |
|---|
| 383 | elif mode == 'comment': |
|---|
| 384 | assert entry is not None |
|---|
| 385 | kw = ief.readComment() |
|---|
| 386 | LinkEntryComment(store=appStore, parent=entry, **kw) |
|---|
| 387 | elif mode == 'metadata': |
|---|
| 388 | assert entry is not None |
|---|
| 389 | kw = ief.readMetadata() |
|---|
| 390 | LinkEntryMetadata(store=appStore, entry=entry, **kw) |
|---|
| 391 | |
|---|
| 392 | |
|---|
| 393 | class InstallPlugin(axiomatic.AxiomaticSubCommand): |
|---|
| 394 | longdesc = 'Install a plugin' |
|---|
| 395 | |
|---|
| 396 | synopsis = '<pluginName>' |
|---|
| 397 | |
|---|
| 398 | def getStore(self): |
|---|
| 399 | return self.parent.getStore() |
|---|
| 400 | |
|---|
| 401 | def getAppStore(self): |
|---|
| 402 | return self.parent.getAppStore() |
|---|
| 403 | |
|---|
| 404 | def parseArgs(self, pluginName): |
|---|
| 405 | self['pluginName'] = self.decodeCommandLine(pluginName) |
|---|
| 406 | |
|---|
| 407 | def postOptions(self): |
|---|
| 408 | plugin.installPlugin(self.getAppStore(), self['pluginName']) |
|---|
| 409 | |
|---|
| 410 | |
|---|
| 411 | class GrantPlugin(axiomatic.AxiomaticSubCommand): |
|---|
| 412 | longdesc = 'Grant a user with access to a plugin' |
|---|
| 413 | |
|---|
| 414 | synopsis = '<user> <domain> <pluginName>' |
|---|
| 415 | |
|---|
| 416 | def parseArgs(self, username, domain, pluginName): |
|---|
| 417 | self['username'] = self.decodeCommandLine(username) |
|---|
| 418 | self['domain'] = self.decodeCommandLine(domain) |
|---|
| 419 | self['pluginName'] = self.decodeCommandLine(pluginName) |
|---|
| 420 | |
|---|
| 421 | def postOptions(self): |
|---|
| 422 | loginSystem = self.parent.getLoginSystem() |
|---|
| 423 | loginAccount = loginSystem.accountByAddress(self['username'], self['domain']) |
|---|
| 424 | assert loginAccount is not None, 'No such user' |
|---|
| 425 | userStore = loginAccount.avatars.open() |
|---|
| 426 | plugin.installPlugin(userStore, self['pluginName']) |
|---|
| 427 | |
|---|
| 428 | |
|---|
| 429 | class ManagePlugins(axiomatic.AxiomaticSubCommand): |
|---|
| 430 | longdesc = 'Manage plugins' |
|---|
| 431 | |
|---|
| 432 | subCommands = [ |
|---|
| 433 | ('install', None, InstallPlugin, 'Install a plugin'), |
|---|
| 434 | ('grant', None, GrantPlugin, 'Endow a user with access to a plugin'), |
|---|
| 435 | ] |
|---|
| 436 | |
|---|
| 437 | def getStore(self): |
|---|
| 438 | return self.parent.getStore() |
|---|
| 439 | |
|---|
| 440 | def getAppStore(self): |
|---|
| 441 | return self.parent.getAppStore() |
|---|
| 442 | |
|---|
| 443 | def getLoginSystem(self): |
|---|
| 444 | return self.parent.getLoginSystem() |
|---|
| 445 | |
|---|
| 446 | |
|---|
| 447 | class Hackery(axiomatic.AxiomaticSubCommand): |
|---|
| 448 | longdesc = 'Beware, thar be hacks!' |
|---|
| 449 | |
|---|
| 450 | def postOptions(self): |
|---|
| 451 | from axiom.scheduler import Scheduler |
|---|
| 452 | from xmantissa.fulltext import SQLiteIndexer |
|---|
| 453 | from xmantissa.ixmantissa import IFulltextIndexer |
|---|
| 454 | from eridanusstd.linkdb import LinkEntrySource, LinkEntryCommentSource |
|---|
| 455 | store = self.parent.getAppStore() |
|---|
| 456 | |
|---|
| 457 | #scheduler = Scheduler(store=store) |
|---|
| 458 | #installOn(scheduler, store) |
|---|
| 459 | |
|---|
| 460 | print 'Deleting old indexers...' |
|---|
| 461 | store.query(SQLiteIndexer).deleteFromStore() |
|---|
| 462 | print 'Creating new indexer...' |
|---|
| 463 | indexer = SQLiteIndexer(store=store) |
|---|
| 464 | store.powerUp(indexer, IFulltextIndexer) |
|---|
| 465 | |
|---|
| 466 | entrySource = store.findOrCreate(LinkEntrySource) |
|---|
| 467 | indexer.addSource(entrySource) |
|---|
| 468 | commentSource = store.findOrCreate(LinkEntryCommentSource) |
|---|
| 469 | indexer.addSource(commentSource) |
|---|
| 470 | |
|---|
| 471 | |
|---|
| 472 | class Eridanus(axiomatic.AxiomaticCommand): |
|---|
| 473 | name = 'eridanus' |
|---|
| 474 | description = 'Eridanus mechanic' |
|---|
| 475 | |
|---|
| 476 | subCommands = [ |
|---|
| 477 | ('service', None, ManageServices, 'Manage services'), |
|---|
| 478 | ('export', None, ExportEntries, 'Export entries'), |
|---|
| 479 | ('import', None, ImportEntries, 'Import entries'), |
|---|
| 480 | ('plugins', None, ManagePlugins, 'Manage plugins'), |
|---|
| 481 | ('hackery', None, Hackery, 'Perform magic'), |
|---|
| 482 | ] |
|---|
| 483 | |
|---|
| 484 | def getStore(self): |
|---|
| 485 | return self.parent.getStore() |
|---|
| 486 | |
|---|
| 487 | def getAppStore(self): |
|---|
| 488 | return self.getLoginSystem().accountByAddress(u'Eridanus', None).avatars.open() |
|---|
| 489 | |
|---|
| 490 | def getLoginSystem(self): |
|---|
| 491 | return IRealm(self.getStore()) |
|---|