feat: Support ENS usernames in messages

This commit is contained in:
Richard Ramos 2020-06-16 17:24:43 -04:00 committed by Iuri Matias
parent ee81c43ddc
commit 0971b5928d
21 changed files with 182 additions and 85 deletions

View File

@ -4,8 +4,8 @@ import ../../status/chat as chat_model
import ../../status/mailservers as mailserver_model import ../../status/mailservers as mailserver_model
import ../../signals/types import ../../signals/types
import ../../status/libstatus/types as status_types import ../../status/libstatus/types as status_types
import ../../signals/types
import ../../status/chat import ../../status/chat
import ../../status/contacts
import ../../status/status import ../../status/status
import views/channels_list import views/channels_list
import view import view
@ -33,8 +33,13 @@ proc handleChatEvents(self: ChatController) =
self.status.events.on("messagesLoaded") do(e:Args): self.status.events.on("messagesLoaded") do(e:Args):
self.view.pushMessages(MsgsLoadedArgs(e).messages) self.view.pushMessages(MsgsLoadedArgs(e).messages)
self.status.events.on("contactUpdate") do(e: Args):
var evArgs = ContactUpdateArgs(e)
self.view.updateUsernames(evArgs.contacts)
self.status.events.on("chatUpdate") do(e: Args): self.status.events.on("chatUpdate") do(e: Args):
var evArgs = ChatUpdateArgs(e) var evArgs = ChatUpdateArgs(e)
self.view.updateUsernames(evArgs.contacts)
self.view.updateChats(evArgs.chats) self.view.updateChats(evArgs.chats)
self.view.pushMessages(evArgs.messages) self.view.pushMessages(evArgs.messages)

View File

@ -7,6 +7,7 @@ import ../../status/status
import ../../status/chat as status_chat import ../../status/chat as status_chat
import ../../status/contacts as status_contacts import ../../status/contacts as status_contacts
import ../../status/chat/[chat, message] import ../../status/chat/[chat, message]
import ../../status/profile/profile
import views/channels_list import views/channels_list
import views/message_list import views/message_list
@ -85,12 +86,19 @@ QtObject:
proc messagePushed*(self: ChatsView) {.signal.} proc messagePushed*(self: ChatsView) {.signal.}
proc pushMessages*(self:ChatsView, messages: seq[Message]) = proc pushMessages*(self:ChatsView, messages: var seq[Message]) =
for msg in messages: for msg in messages.mitems:
self.upsertChannel(msg.chatId) self.upsertChannel(msg.chatId)
msg.alias = self.status.chat.getUserName(msg.fromAuthor, msg.alias)
self.messageList[msg.chatId].add(msg) self.messageList[msg.chatId].add(msg)
self.messagePushed() self.messagePushed()
proc updateUsernames*(self:ChatsView, contacts: seq[Profile]) =
if contacts.len > 0:
# Updating usernames for all the messages list
for k in self.messageList.keys:
self.messageList[k].updateUsernames(contacts)
proc getMessageList(self: ChatsView): QVariant {.slot.} = proc getMessageList(self: ChatsView): QVariant {.slot.} =
self.upsertChannel(self.activeChannel.id) self.upsertChannel(self.activeChannel.id)
return newQVariant(self.messageList[self.activeChannel.id]) return newQVariant(self.messageList[self.activeChannel.id])

View File

@ -1,6 +1,8 @@
import NimQml, Tables import NimQml, Tables
import ../../../status/chat import ../../../status/chat
import ../../../status/chat/[message,stickers] import ../../../status/chat/[message,stickers]
import ../../../status/profile/profile
import ../../../status/ens
type type
ChatMessageRoles {.pure.} = enum ChatMessageRoles {.pure.} = enum
@ -34,7 +36,6 @@ QtObject:
result.contentType = ContentType.ChatIdentifier; result.contentType = ContentType.ChatIdentifier;
result.chatId = chatId result.chatId = chatId
proc newChatMessageList*(chatId: string): ChatMessageList = proc newChatMessageList*(chatId: string): ChatMessageList =
new(result, delete) new(result, delete)
result.messages = @[result.chatIdentifier(chatId)] result.messages = @[result.chatIdentifier(chatId)]
@ -99,3 +100,17 @@ QtObject:
self.messages.add(message) self.messages.add(message)
self.endInsertRows() self.endInsertRows()
proc updateUsernames*(self: ChatMessageList, contacts: seq[Profile]) =
let topLeft = self.createIndex(0, 0, nil)
let bottomRight = self.createIndex(self.messages.len, 0, nil)
# TODO: change this once the contact list uses a table
for c in contacts:
for m in self.messages.mitems:
if m.fromAuthor == c.id:
m.alias = userNameOrAlias(c)
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.Username.int])

View File

@ -4,14 +4,14 @@ import ../../status/libstatus/mailservers as status_mailservers
import ../../signals/types import ../../signals/types
import "../../status/libstatus/types" as status_types import "../../status/libstatus/types" as status_types
import ../../status/libstatus/settings as status_settings import ../../status/libstatus/settings as status_settings
import json import ../../status/profile/[profile, mailserver]
import ../../status/profile
import ../../status/contacts import ../../status/contacts
import ../../status/status import ../../status/status
import ../../status/chat as status_chat
import ../../status/chat/chat
import view import view
type ProfileController* = object type ProfileController* = ref object of SignalSubscriber
view*: ProfileView view*: ProfileView
variant*: QVariant variant*: QVariant
status*: Status status*: Status
@ -42,6 +42,13 @@ proc init*(self: ProfileController, account: Account) =
let mailserver = MailServer(name: mailserver_config[0], endpoint: mailserver_config[1]) let mailserver = MailServer(name: mailserver_config[0], endpoint: mailserver_config[1])
self.view.addMailServerToList(mailserver) self.view.addMailServerToList(mailserver)
let contactList = self.status.contacts.getContacts().elems for contact in self.status.contacts.getContacts():
for contact in contactList: self.view.addContactToList(contact)
self.view.addContactToList(contact.toProfileModel())
method onSignal(self: ProfileController, data: Signal) =
let msgData = MessageSignal(data);
if msgData.contacts.len > 0:
# TODO: view should react to model changes
self.status.chat.updateContacts(msgData.contacts)
self.view.updateContactList(msgData.contacts)

View File

@ -2,9 +2,11 @@ import NimQml
import views/mailservers_list import views/mailservers_list
import views/contact_list import views/contact_list
import views/profile_info import views/profile_info
import ../../status/profile import ../../status/profile/[mailserver, profile]
import ../../status/profile as status_profile
import ../../status/accounts as status_accounts import ../../status/accounts as status_accounts
import ../../status/status import ../../status/status
import ../../status/chat/chat
QtObject: QtObject:
type ProfileView* = ref object of QObject type ProfileView* = ref object of QObject
@ -37,6 +39,10 @@ QtObject:
QtProperty[QVariant] mailserversList: QtProperty[QVariant] mailserversList:
read = getMailserversList read = getMailserversList
proc updateContactList*(self: ProfileView, contacts: seq[Profile]) =
for contact in contacts:
self.contactList.updateContact(contact)
proc addContactToList*(self: ProfileView, contact: Profile) = proc addContactToList*(self: ProfileView, contact: Profile) =
self.contactList.addContactToList(contact) self.contactList.addContactToList(contact)

View File

@ -1,7 +1,7 @@
import NimQml import NimQml
import Tables import Tables
import strformat import strformat
import ../../../status/profile import ../../../status/profile/profile
from ../../../status/ens import nil from ../../../status/ens import nil
type type
@ -20,6 +20,9 @@ QtObject:
proc newContactList*(): ContactList = proc newContactList*(): ContactList =
new(result, delete) new(result, delete)
# TODO: (rramos) contacts should be a table[string, Profile] instead, with the key being the public key
# This is to optimize determining if a contact is part of the contact list or not
# (including those that do not have a system tag)
result.contacts = @[] result.contacts = @[]
result.setup result.setup
@ -28,10 +31,16 @@ QtObject:
proc getUserName(contact: Profile): string = proc getUserName(contact: Profile): string =
if(contact.ensName != "" and contact.ensVerified): if(contact.ensName != "" and contact.ensVerified):
result = "@" & ens.userName(contact.ensName) result = "@" & ens.userName(contact.ensName, true)
else: else:
result = contact.alias result = contact.alias
proc userName(self: ContactList, pubKey: string, defaultValue: string = ""): string {.slot.} =
for contact in self.contacts:
if(contact.id != pubKey): continue
return getUserName(contact)
return defaultValue
method data(self: ContactList, index: QModelIndex, role: int): QVariant = method data(self: ContactList, index: QModelIndex, role: int): QVariant =
if not index.isValid: if not index.isValid:
return return
@ -54,3 +63,21 @@ QtObject:
self.beginInsertRows(newQModelIndex(), self.contacts.len, self.contacts.len) self.beginInsertRows(newQModelIndex(), self.contacts.len, self.contacts.len)
self.contacts.add(contact) self.contacts.add(contact)
self.endInsertRows() self.endInsertRows()
proc updateContact*(self: ContactList, contact: Profile) =
var found = false
let topLeft = self.createIndex(0, 0, nil)
let bottomRight = self.createIndex(self.contacts.len, 0, nil)
for c in self.contacts:
if(c.id != contact.id): continue
found = true
c.ensName = contact.ensName
c.ensVerified = contact.ensVerified
if not found:
self.addContactToList(contact)
else:
self.dataChanged(topLeft, bottomRight, @[ContactRoles.Name.int])

View File

@ -1,6 +1,6 @@
import NimQml import NimQml
import Tables import Tables
import ../../../status/profile import ../../../status/profile/[profile, mailserver]
type type
MailServerRoles {.pure.} = enum MailServerRoles {.pure.} = enum

View File

@ -1,5 +1,5 @@
import NimQml import NimQml
import ../../../status/profile import ../../../status/profile/profile
QtObject: QtObject:
type ProfileInfoView* = ref object of QObject type ProfileInfoView* = ref object of QObject

View File

@ -93,6 +93,7 @@ proc mainProc() =
signalController.addSubscriber(SignalType.Wallet, wallet) signalController.addSubscriber(SignalType.Wallet, wallet)
signalController.addSubscriber(SignalType.Wallet, node) signalController.addSubscriber(SignalType.Wallet, node)
signalController.addSubscriber(SignalType.Message, chat) signalController.addSubscriber(SignalType.Message, chat)
signalController.addSubscriber(SignalType.Message, profile)
signalController.addSubscriber(SignalType.DiscoverySummary, chat) signalController.addSubscriber(SignalType.DiscoverySummary, chat)
signalController.addSubscriber(SignalType.NodeLogin, login) signalController.addSubscriber(SignalType.NodeLogin, login)
signalController.addSubscriber(SignalType.NodeLogin, onboarding) signalController.addSubscriber(SignalType.NodeLogin, onboarding)

View File

@ -2,8 +2,8 @@ import json
import types import types
import ../status/libstatus/accounts as status_accounts import ../status/libstatus/accounts as status_accounts
import ../status/chat/[chat, message] import ../status/chat/[chat, message]
import ../status/profile/profile
import random import random
import tables
proc toMessage*(jsonMsg: JsonNode): Message proc toMessage*(jsonMsg: JsonNode): Message
@ -12,16 +12,11 @@ proc toChat*(jsonChat: JsonNode): Chat
proc fromEvent*(event: JsonNode): Signal = proc fromEvent*(event: JsonNode): Signal =
var signal:MessageSignal = MessageSignal() var signal:MessageSignal = MessageSignal()
signal.messages = @[] signal.messages = @[]
signal.contacts = initTable[string, ChatContact]() signal.contacts = @[]
if event["event"]{"contacts"} != nil: if event["event"]{"contacts"} != nil:
for jsonContact in event["event"]["contacts"]: for jsonContact in event["event"]["contacts"]:
let contact = ChatContact( signal.contacts.add(jsonContact.toProfileModel())
id: jsonContact["id"].getStr,
name: jsonContact["name"].getStr,
ensVerified: jsonContact["ensVerified"].getBool
)
signal.contacts[contact.id] = contact
if event["event"]{"messages"} != nil: if event["event"]{"messages"} != nil:
for jsonMsg in event["event"]["messages"]: for jsonMsg in event["event"]["messages"]:

View File

@ -2,6 +2,7 @@ import json
import chronicles import chronicles
import ../status/libstatus/types import ../status/libstatus/types
import ../status/chat/[chat, message] import ../status/chat/[chat, message]
import ../status/profile/profile
import json_serialization import json_serialization
import tables import tables
@ -26,7 +27,7 @@ method onSignal*(self: SignalSubscriber, data: Signal) {.base.} =
type MessageSignal* = ref object of Signal type MessageSignal* = ref object of Signal
messages*: seq[Message] messages*: seq[Message]
chats*: seq[Chat] chats*: seq[Chat]
contacts*: Table[string, ChatContact] contacts*: seq[Profile]
type Filter* = object type Filter* = object
chatId*: string chatId*: string

View File

@ -1,16 +1,18 @@
import eventemitter, json import eventemitter, json
import sequtils import sequtils
import libstatus/chat as status_chat import libstatus/chat as status_chat
import ./profile as status_profile
import chronicles import chronicles
import profile/profile
import chat/[chat, message] import chat/[chat, message]
import ../signals/messages import ../signals/messages
import tables import tables
import ens
type type
ChatUpdateArgs* = ref object of Args ChatUpdateArgs* = ref object of Args
chats*: seq[Chat] chats*: seq[Chat]
messages*: seq[Message] messages*: seq[Message]
contacts*: seq[Profile]
ChatIdArg* = ref object of Args ChatIdArg* = ref object of Args
chatId*: string chatId*: string
@ -29,6 +31,7 @@ type
ChatModel* = ref object ChatModel* = ref object
events*: EventEmitter events*: EventEmitter
contacts*: Table[string, Profile]
channels*: Table[string, Chat] channels*: Table[string, Chat]
filters*: Table[string, string] filters*: Table[string, string]
msgCursor*: Table[string, string] msgCursor*: Table[string, string]
@ -36,6 +39,7 @@ type
proc newChatModel*(events: EventEmitter): ChatModel = proc newChatModel*(events: EventEmitter): ChatModel =
result = ChatModel() result = ChatModel()
result.events = events result.events = events
result.contacts = initTable[string, Profile]()
result.channels = initTable[string, Chat]() result.channels = initTable[string, Chat]()
result.filters = initTable[string, string]() result.filters = initTable[string, string]()
result.msgCursor = initTable[string, string]() result.msgCursor = initTable[string, string]()
@ -48,7 +52,7 @@ proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message]) =
if chat.isActive: if chat.isActive:
self.channels[chat.id] = chat self.channels[chat.id] = chat
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
proc hasChannel*(self: ChatModel, chatId: string): bool = proc hasChannel*(self: ChatModel, chatId: string): bool =
self.channels.hasKey(chatId) self.channels.hasKey(chatId)
@ -124,7 +128,7 @@ proc leave*(self: ChatModel, chatId: string) =
if self.channels[chatId].chatType == ChatType.PrivateGroupChat: if self.channels[chatId].chatType == ChatType.PrivateGroupChat:
let leaveGroupResponse = status_chat.leaveGroupChat(chatId) let leaveGroupResponse = status_chat.leaveGroupChat(chatId)
var (chats, messages) = self.processChatUpdate(parseJson(leaveGroupResponse)) var (chats, messages) = self.processChatUpdate(parseJson(leaveGroupResponse))
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
# We still want to be able to receive messages unless we block the 1:1 sender # We still want to be able to receive messages unless we block the 1:1 sender
if self.filters.hasKey(chatId) and self.channels[chatId].chatType == ChatType.Public: if self.filters.hasKey(chatId) and self.channels[chatId].chatType == ChatType.Public:
@ -155,7 +159,7 @@ proc formatChatUpdate(response: JsonNode): (seq[Chat], seq[Message]) =
proc sendMessage*(self: ChatModel, chatId: string, msg: string): string = proc sendMessage*(self: ChatModel, chatId: string, msg: string): string =
var sentMessage = status_chat.sendChatMessage(chatId, msg) var sentMessage = status_chat.sendChatMessage(chatId, msg)
var (chats, messages) = self.processChatUpdate(parseJson(sentMessage)) var (chats, messages) = self.processChatUpdate(parseJson(sentMessage))
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
sentMessage sentMessage
proc chatMessages*(self: ChatModel, chatId: string, initialLoad:bool = true) = proc chatMessages*(self: ChatModel, chatId: string, initialLoad:bool = true) =
@ -177,9 +181,20 @@ proc markAllChannelMessagesRead*(self: ChatModel, chatId: string): JsonNode =
proc confirmJoiningGroup*(self: ChatModel, chatId: string) = proc confirmJoiningGroup*(self: ChatModel, chatId: string) =
var response = parseJson(status_chat.confirmJoiningGroup(chatId)) var response = parseJson(status_chat.confirmJoiningGroup(chatId))
var (chats, messages) = self.processChatUpdate(response) var (chats, messages) = self.processChatUpdate(response)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
proc renameGroup*(self: ChatModel, chatId: string, newName: string) = proc renameGroup*(self: ChatModel, chatId: string, newName: string) =
var response = parseJson(status_chat.renameGroup(chatId, newName)) var response = parseJson(status_chat.renameGroup(chatId, newName))
var (chats, messages) = formatChatUpdate(response) var (chats, messages) = formatChatUpdate(response)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
proc getUserName*(self: ChatModel, id: string, defaultUserName: string):string =
if(self.contacts.hasKey(id)):
return userNameOrAlias(self.contacts[id])
else:
return defaultUserName
proc updateContacts*(self: ChatModel, contacts: seq[Profile]) =
for c in contacts:
self.contacts[c.id] = c
self.events.emit("chatUpdate", ChatUpdateArgs(contacts: contacts))

View File

@ -11,11 +11,6 @@ type ChatType* {.pure.}= enum
proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne
type ChatContact* = object
id*: string
name*: string
ensVerified*: bool
type ChatMember* = object type ChatMember* = object
admin*: bool admin*: bool
id*: string id*: string

View File

@ -1,12 +1,17 @@
import eventemitter import eventemitter
import json import json
import libstatus/contacts as status_contacts import libstatus/contacts as status_contacts
import profile import profile/profile
import sequtils
type type
ContactModel* = ref object ContactModel* = ref object
events*: EventEmitter events*: EventEmitter
type
ContactUpdateArgs* = ref object of Args
contacts*: seq[Profile]
proc newContactModel*(events: EventEmitter): ContactModel = proc newContactModel*(events: EventEmitter): ContactModel =
result = ContactModel() result = ContactModel()
result.events = events result.events = events
@ -20,8 +25,9 @@ proc blockContact*(self: ContactModel, id: string): string =
contact.systemTags.add(":contact/blocked") contact.systemTags.add(":contact/blocked")
status_contacts.blockContact(contact) status_contacts.blockContact(contact)
proc getContacts*(self: ContactModel): JsonNode = proc getContacts*(self: ContactModel): seq[Profile] =
status_contacts.getContacts() result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel())
self.events.emit("contactUpdate", ContactUpdateArgs(contacts: result))
proc addContact*(self: ContactModel, id: string): string = proc addContact*(self: ContactModel, id: string): string =
let contact = self.getContactByID(id) let contact = self.getContactByID(id)

View File

@ -1,9 +1,19 @@
import strutils import strutils
import profile/profile
let domain* = ".statusnet.eth" let domain* = ".stateofus.eth"
proc userName*(ensName: string): string = proc userName*(ensName: string, removeSuffix: bool = false): string =
if ensName != "" and ensName.endsWith(domain): if ensName != "" and ensName.endsWith(domain):
if removeSuffix:
result = ensName.split(".")[0] result = ensName.split(".")[0]
else: else:
result = ensName result = ensName
else:
result = ensName
proc userNameOrAlias*(contact: Profile): string =
if(contact.ensName != "" and contact.ensVerified):
result = "@" & userName(contact.ensName, true)
else:
result = contact.alias

View File

@ -1,7 +1,7 @@
import core import core
import json import json
import utils import utils
import ../profile import ../profile/profile
# TODO: remove Profile from here # TODO: remove Profile from here
proc blockContact*(contact: Profile): string = proc blockContact*(contact: Profile): string =

View File

@ -1,51 +1,10 @@
import json import json
import eventemitter import eventemitter
import libstatus/types import libstatus/types
import profile/profile
import libstatus/core as libstatus_core import libstatus/core as libstatus_core
import libstatus/accounts as status_accounts import libstatus/accounts as status_accounts
type
MailServer* = ref object
name*, endpoint*: string
type Profile* = ref object
id*, alias*, username*, identicon*, address*, ensName*: string
ensVerified*: bool
ensVerifiedAt*: int
ensVerificationRetries*: int
systemTags*: seq[string]
proc toProfileModel*(account: Account): Profile =
result = Profile(
id: "",
username: account.name,
identicon: account.photoPath,
alias: account.name,
ensName: "",
ensVerified: false,
ensVerifiedAt: 0,
ensVerificationRetries: 0,
systemTags: @[]
)
proc toProfileModel*(profile: JsonNode): Profile =
var systemTags: seq[string] = @[]
if profile["systemTags"].kind != JNull:
systemTags = profile["systemTags"].to(seq[string])
result = Profile(
id: profile["id"].str,
username: profile["alias"].str,
identicon: profile["identicon"].str,
address: profile["id"].str,
alias: profile["alias"].str,
ensName: profile["name"].str,
ensVerified: profile["ensVerified"].getBool,
ensVerifiedAt: profile["ensVerifiedAt"].getInt,
ensVerificationRetries: profile["ensVerificationRetries"].getInt,
systemTags: systemTags
)
type type
ProfileModel* = ref object ProfileModel* = ref object

View File

@ -0,0 +1,4 @@
type
MailServer* = ref object
name*, endpoint*: string

View File

@ -0,0 +1,41 @@
import ../libstatus/types
import json
type Profile* = ref object
id*, alias*, username*, identicon*, address*, ensName*: string
ensVerified*: bool
ensVerifiedAt*: int
ensVerificationRetries*: int
systemTags*: seq[string]
proc toProfileModel*(account: Account): Profile =
result = Profile(
id: "",
username: account.name,
identicon: account.photoPath,
alias: account.name,
ensName: "",
ensVerified: false,
ensVerifiedAt: 0,
ensVerificationRetries: 0,
systemTags: @[]
)
proc toProfileModel*(profile: JsonNode): Profile =
var systemTags: seq[string] = @[]
if profile["systemTags"].kind != JNull:
systemTags = profile["systemTags"].to(seq[string])
result = Profile(
id: profile["id"].str,
username: profile["alias"].str,
identicon: profile["identicon"].str,
address: profile["id"].str,
alias: profile["alias"].str,
ensName: profile["name"].str,
ensVerified: profile["ensVerified"].getBool,
ensVerifiedAt: profile["ensVerifiedAt"].getInt,
ensVerificationRetries: profile["ensVerificationRetries"].getInt,
systemTags: systemTags
)

View File

@ -116,6 +116,7 @@ ScrollView {
Message { Message {
id: msgDelegate id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId chatId: model.chatId
userName: model.userName userName: model.userName
message: model.message message: model.message

View File

@ -8,6 +8,7 @@ import "../../../../imports"
import "../components" import "../components"
Item { Item {
property string fromAuthor: "0x0011223344556677889910"
property string userName: "Jotaro Kujo" property string userName: "Jotaro Kujo"
property string message: "That's right. We're friends... Of justice, that is." property string message: "That's right. We're friends... Of justice, that is."
property string identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" property string identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="