Browse Source

initial commit

htmlescape
erinn 1 year ago
parent
commit
8a9ba6d154
30 changed files with 1067 additions and 643 deletions
  1. +0
    -237
      gcd.go
  2. +39
    -0
      go/characters/cwtchlistener.go
  3. +21
    -0
      go/characters/grouppoller.go
  4. +39
    -0
      go/characters/postmanpat.go
  5. +49
    -0
      go/characters/presencepoller.go
  6. +35
    -0
      go/characters/torstatuspoller.go
  7. +3
    -0
      go/constants/style.go
  8. +24
    -9
      go/cwtchthings/chatchannellistener.go
  9. +29
    -0
      go/cwutil/utils.go
  10. +11
    -0
      go/gobjects/contact.go
  11. +8
    -0
      go/gobjects/letter.go
  12. +14
    -0
      go/gobjects/message.go
  13. +303
    -0
      go/gothings/gcd.go
  14. +116
    -0
      go/gothings/uistate.go
  15. +11
    -0
      go/the/globals.go
  16. +106
    -274
      main.go
  17. +5
    -55
      qml/main.qml
  18. +53
    -0
      qml/panes/AddGroupPane.qml
  19. +68
    -0
      qml/panes/GroupSettingsPane.qml
  20. +50
    -0
      qml/panes/SettingsPane.qml
  21. +18
    -18
      qml/widgets/ContactList.qml
  22. +16
    -23
      qml/widgets/ContactRow.qml
  23. +6
    -4
      qml/widgets/Message.qml
  24. +23
    -16
      qml/widgets/MessageList.qml
  25. +16
    -3
      qml/widgets/MyProfile.qml
  26. BIN
      qml/widgets/MyProfile.qmlc
  27. +1
    -1
      qml/widgets/ScalingLabel.qml
  28. +2
    -2
      qml/widgets/SimpleButton.qml
  29. BIN
      qml/widgets/SimpleButton.qmlc
  30. +1
    -1
      qml/widgets/StackToolbar.qml

+ 0
- 237
gcd.go View File

@@ -1,237 +0,0 @@
package main

import (
"cwtch.im/cwtch/model"
"encoding/base32"
"github.com/therecipe/qt/core"
"log"
"strings"
"time"
"fmt"
)

var TIME_FORMAT = "Mon 3:04pm"

type GrandCentralDispatcher struct {
core.QObject

currentOpenConversation string `property:"currentOpenConversation"`
themeScale float32 `property:"themeScale"`

// messages pane stuff
_ func(from, message, displayname string, mID uint, ts, source string) `signal:"AppendMessage"`
_ func() `signal:"ClearMessages"`
_ func() `signal:"ResetMessagePane"`
_ func(uint) `signal:"Acknowledged"`

// contact list stuff
_ func(onion string, num int) `signal:"SetUnread"`
_ func(onion string, status int) `signal:"SetConnectionStatus"`
_ func(name, onion, server, image, badge string, trusted bool) `signal:"AddContact"`
_ func(onion string) `signal:"MarkTrusted"`

// profile-area stuff
_ func(name, onion, image string) `signal:"UpdateMyProfile"`
_ func(status int, str string) `signal:"TorStatus"`

// other stuff i can't ontologize atm
_ func(str string) `signal:"InvokePopup"`

// exfiltrated signals (written in go, below)
_ func(message string, mid uint) `signal:"sendMessage,auto"`
_ func(onion string) `signal:"loadMessagesPane,auto"`
_ func(signal string) `signal:"broadcast,auto"` // convenience relay signal
_ func(str string) `signal:"importString,auto"`
_ func(str string) `signal:"popup,auto"`
_ func(nick string) `signal:"updateNick,auto"`
}

func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) {
if len(message) > 65530 {
gcd.InvokePopup("message is too long")
return
}

if gcd.currentOpenConversation == "" {
return
}

if len(gcd.currentOpenConversation) == 32 { // SEND TO GROUP
if !peer.GetGroup(gcd.currentOpenConversation).Accepted {
peer.GetGroup(gcd.currentOpenConversation).Accepted = true
peer.Save()
}

peer.SendMessageToGroup(gcd.currentOpenConversation, message)
return
}

// TODO: require explicit invite accept/reject instead of implicitly trusting on send
if !peer.GetContact(gcd.currentOpenConversation).Trusted {
peer.GetContact(gcd.currentOpenConversation).Trusted = true
peer.Save()
gcd.MarkTrusted(gcd.currentOpenConversation)
}

select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages
// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
case outgoingMessages <- Message{gcd.currentOpenConversation, message, true, mID, time.Now()}:
default:
}

DeliverMessageToUI(gcd.currentOpenConversation, gcd.currentOpenConversation, "", message, mID, true, time.Now())
}

func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
gcd.ClearMessages()
gcd.currentOpenConversation = onion
gcd.SetUnread(onion, 0)

if len(onion) == 32 { // LOAD GROUP
log.Printf("LOADING GROUP %s", onion)
tl := peer.GetGroup(onion).GetTimeline()
log.Printf("messages: %d", len(tl))
for i := range tl {
var handle string
if tl[i].PeerID == peer.GetProfile().Onion {
handle = "me"
} else {
handle = tl[i].PeerID
}
var name string
var exists bool
name, exists = peer.GetProfile().GetCustomAttribute(tl[i].PeerID + "_name")
if !exists || name == "" {
name = tl[i].PeerID[:16] + "..."
}
gcd.AppendMessage(handle, tl[i].Message, name, 0, tl[i].Timestamp.Format(TIME_FORMAT), randomProfileImage(tl[i].PeerID))
}
return
} // ELSE LOAD CONTACT

_, exists := contactMgr[onion]
if exists { // (if not, they haven't been accepted as a contact yet)
contactMgr[onion].Unread = 0

messages := contactMgr[onion].Messages
for i := range messages {
from := messages[i].With
if messages[i].FromMe {
from = "me"
}
gcd.AppendMessage(from, messages[i].Message, "", messages[i].MessageID, messages[i].Timestamp.Format(TIME_FORMAT), randomProfileImage(onion))
}
}
}

func (this *GrandCentralDispatcher) broadcast(signal string) {
switch signal {
default:
log.Printf("unhandled broadcast signal: %v", signal)
case "ResetMessagePane":
gcd.ResetMessagePane()
}
}

func (this *GrandCentralDispatcher) importString(str string) {
if len(str) < 5 {
log.Printf("ignoring short string")
return
}

log.Printf("importing: %s\n", str)
onion := str
name := onion
str = strings.TrimSpace(str)

//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
if str[0:5] == "torv3" { // GROUP INVITE
groupID, err := peer.ImportGroup(str)
if err != nil {
gcd.InvokePopup("not a valid group invite")
return
}

group := peer.GetGroup(groupID)
peer.JoinServer(group.GroupServer)
peer.Save()
fmt.Printf("imported groupid=%s server=%s", groupID, group.GroupServer)

return
}

if strings.Contains(str, " ") { // usually people prepend spaces and we don't want it going into the name (use ~ for that)
parts := strings.Split(strings.TrimSpace(str), " ")
str = parts[len(parts)-1]
}

if strings.Contains(str, "~") {
parts := strings.Split(str, "~")
onion = parts[len(parts)-1]
name = strings.Join(parts[:len(parts)-1], " ")
}

if len(onion) != 56 {
gcd.InvokePopup("invalid format")
return
}

name = strings.TrimSpace(name)
if name == "" {
gcd.InvokePopup("empty name")
return
}

if len(name) > 32 {
name = name[:32] //TODO: better strategy for long names?
}

decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
if err != nil {
log.Printf("%v", err)
gcd.InvokePopup("bad format. missing characters?")
return
}

_, exists := peer.GetProfile().GetCustomAttribute(name + "_onion")
if exists {
gcd.InvokePopup("can't re-use names :(")
return
}

_, exists = peer.GetProfile().GetCustomAttribute(onion + "_name")
if exists {
gcd.InvokePopup("already have this contact")
return //TODO: bring them to the duplicate
}

pp := model.PublicProfile{
name,
decodedPub[:32],
false,
false,
onion}

if peer == nil {
log.Printf("[!!!] peer is nil?!?!?")
}

log.Printf("adding %v <%v>", name, onion)
peer.GetProfile().Contacts[onion] = &pp
peer.GetProfile().SetCustomAttribute(onion+"_name", name)
peer.GetProfile().SetCustomAttribute(name+"_onion", onion)
peer.GetProfile().TrustPeer(onion)
peer.Save()
go peer.PeerWithOnion(onion)
//contactMgr[onion] = &Contact{[]Message{}, 0, 0}
//gcd.AddContact(name, onion, randomProfileImage(onion), "0")
}

func (this *GrandCentralDispatcher) popup(str string) {
gcd.InvokePopup(str)
}

func (this *GrandCentralDispatcher) updateNick(nick string) {
peer.GetProfile().Name = nick
peer.Save()
}

+ 39
- 0
go/characters/cwtchlistener.go View File

@@ -0,0 +1,39 @@
package characters

import (
"cwtch.im/cwtch/model"
"bounce/go/the"
"bounce/go/gobjects"
)

func CwtchListener(callback func(message *gobjects.Message), groupID string, channel chan model.Message) {
for {
m := <-channel

name := m.PeerID
if name == the.Peer.GetProfile().Onion {
name = "me"
} else {
var exists bool // lol this is a golang antifeature
name, exists = the.Peer.GetContact(m.PeerID).GetAttribute("name")
if !exists {
name = ""
}
}

if name == "" {
name = m.PeerID[:16] + "..."
}

callback(&gobjects.Message{
groupID,
m.PeerID,
name,
m.Message,
"",
m.PeerID == the.Peer.GetProfile().Onion,
0,
m.Timestamp,
})
}
}

+ 21
- 0
go/characters/grouppoller.go View File

@@ -0,0 +1,21 @@
package characters

import (
"time"
"bounce/go/the"
"bounce/go/gobjects"
)

func GroupPoller(getContact func(string) *gobjects.Contact, updateContact func(string)) {
for {
time.Sleep(time.Second * 4)

servers := the.Peer.GetServers()
groups := the.Peer.GetGroups()
for i := range groups {
group := the.Peer.GetGroup(groups[i])
getContact(group.GroupID).Status = int(servers[group.GroupServer])
updateContact(group.GroupID)
}
}
}

+ 39
- 0
go/characters/postmanpat.go View File

@@ -0,0 +1,39 @@
package characters

import (
"bounce/go/the"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"log"
"bounce/go/gobjects"
)

func PostmanPat(messages chan gobjects.Letter) {
postOffice := make(map[string]chan gobjects.Letter)

for {
m := <-messages

_, found := postOffice[m.To]
if !found {
postOffice[m.To] = make(chan gobjects.Letter, 100)
go andHisBlackAndWhiteCat(postOffice[m.To])
}

postOffice[m.To] <- m
}
}

func andHisBlackAndWhiteCat(incomingMessages chan gobjects.Letter) {
for {
m := <-incomingMessages
connection := the.Peer.PeerWithOnion(m.To)
connection.WaitTilAuthenticated()
connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) {
chatchannel, ok := channel.Handler.(*channels.ChatChannel)
if ok {
log.Printf("Sending packet")
the.AcknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MID
}
})
}
}

+ 49
- 0
go/characters/presencepoller.go View File

@@ -0,0 +1,49 @@
package characters

import (
"time"
"bounce/go/the"
"bounce/go/cwutil"
"bounce/go/gobjects"
)

func PresencePoller(getContact func(string) *gobjects.Contact, addContact func(contact *gobjects.Contact), updateContact func(string)) { // TODO: make this subscribe-able in ricochet
time.Sleep(time.Second * 4)
for {
contacts := the.Peer.GetContacts()
for i := range contacts {
ct := getContact(contacts[i])
if ct == nil { // new contact has attempted to connect with us, treat it as an invite
toc := the.Peer.GetContact(contacts[i])
c, _ := the.Peer.GetProfile().GetContact(contacts[i])
addContact(&gobjects.Contact{
toc.Onion,
toc.Name,
cwutil.RandomProfileImage(toc.Onion),
"",
0,
0,
c.Trusted,
})
c.SetAttribute("name", c.Name)
the.CwtchApp.SaveProfile(the.Peer)
}

cxnState, found := the.Peer.GetPeers()[contacts[i]]
if !found {
c2 := getContact(contacts[i])
if c2 != nil && c2.Status != -2 {
c2.Status = -2
updateContact(contacts[i])
}
} else {
c2 := getContact(contacts[i])
if c2 != nil && c2.Status != int(cxnState) {
c2.Status = int(cxnState)
updateContact(contacts[i])
}
}
}
time.Sleep(time.Second * 4)
}
}

+ 35
- 0
go/characters/torstatuspoller.go View File

@@ -0,0 +1,35 @@
package characters

import (
"git.openprivacy.ca/openprivacy/asaur"
"strconv"
"time"
)

func TorStatusPoller(setTorStatus func(int, string)) {
for {
time.Sleep(time.Second)
//todo: this should use a config manager
//todo: also, try dialing the proxy to differentiate tor not running vs control port not configured
rawStatus, err := asaur.GetInfo("localhost:9051", "tcp4", "", "status/bootstrap-phase")
if err != nil {
setTorStatus(0, "can't find tor. is it running? is the controlport configured?")
continue
}

status := asaur.ParseBootstrapPhase(rawStatus)
progress, _ := strconv.Atoi(status["PROGRESS"])

if status["TAG"] == "done" {
setTorStatus(3, "tor appears to be running just fine!")
continue
}

if progress == 0 {
setTorStatus(1, "tor is trying to start up")
continue
}

setTorStatus(2, status["SUMMARY"])
}
}

+ 3
- 0
go/constants/style.go View File

@@ -0,0 +1,3 @@
package constants

var TIME_FORMAT = "Mon 3:04pm"

chatchannellistener.go → go/cwtchthings/chatchannellistener.go View File

@@ -1,20 +1,26 @@
package main
package cwtchthings

import (
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"time"
)
"bounce/go/the"
"bounce/go/cwutil"
"bounce/go/gobjects"
)

type ChatChannelListener struct {
rai *application.ApplicationInstance
ra *application.RicochetApplication
addMessage func(*gobjects.Message)
acknowledged func(uint)
}


func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, addMessage func(*gobjects.Message), acknowledged func(uint)) {
this.rai = rai
this.ra = ra
this.addMessage = addMessage
this.acknowledged = acknowledged
}

// We always want bidirectional chat channels
@@ -32,14 +38,23 @@ func (this *ChatChannelListener) OpenInbound() {
}

func (this *ChatChannelListener) ChatMessage(messageID uint32, when time.Time, message string) bool {
DeliverMessageToUI(this.rai.RemoteHostname, this.rai.RemoteHostname, "", message, uint(messageID), false, when)
go func() {
this.addMessage(&gobjects.Message{
this.rai.RemoteHostname,
this.rai.RemoteHostname,
"",
message,
cwutil.RandomProfileImage(this.rai.RemoteHostname),
false,
int(messageID),
when,
})
go func() { // TODO: this is probably no longer necessary. check later
time.Sleep(time.Second)
peer.Save()
the.CwtchApp.SaveProfile(the.Peer)
}()
return true
}

func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) {
gcd.Acknowledged(acknowledgementIDs[messageID])
this.acknowledged(the.AcknowledgementIDs[messageID])
}

+ 29
- 0
go/cwutil/utils.go View File

@@ -0,0 +1,29 @@
package cwutil

import (
"encoding/base32"
"encoding/hex"
"fmt"
"strings"
)

// temporary until we do real picture selection
func RandomProfileImage(onion string) string {
choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"}
barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err != nil || len(barr) != 35 {
fmt.Printf("error: %v %v %v\n", onion, err, barr)
return "qrc:/qml/images/extra/openprivacy.png"
}
return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png"
}

func RandomGroupImage(handle string) string {
choices := []string{"001-borobudur", "002-opera-house", "003-burj-al-arab", "004-chrysler", "005-acropolis", "006-empire-state-building", "007-temple", "008-indonesia-1", "009-new-zealand", "010-notre-dame", "011-space-needle", "012-seoul", "013-mosque", "014-milan", "015-statue", "016-pyramid", "017-cologne", "018-brandenburg-gate", "019-berlin-cathedral", "020-hungarian-parliament", "021-buckingham", "022-thailand", "023-independence", "024-angkor-wat", "025-vaticano", "026-christ-the-redeemer", "027-colosseum", "028-golden-gate-bridge", "029-sphinx", "030-statue-of-liberty", "031-cradle-of-humankind", "032-istanbul", "033-london-eye", "034-sagrada-familia", "035-tower-bridge", "036-burj-khalifa", "037-washington", "038-big-ben", "039-stonehenge", "040-white-house", "041-ahu-tongariki", "042-capitol", "043-eiffel-tower", "044-church-of-the-savior-on-spilled-blood", "045-arc-de-triomphe", "046-windmill", "047-louvre", "048-torii-gate", "049-petronas", "050-matsumoto-castle", "051-fuji", "052-temple-of-heaven", "053-pagoda", "054-chichen-itza", "055-forbidden-city", "056-merlion", "057-great-wall-of-china", "058-taj-mahal", "059-pisa", "060-indonesia"}
barr, err := hex.DecodeString(handle)
if err != nil || len(barr) == 0 {
fmt.Printf("error: %v %v %v\n", handle, err, barr)
return "qrc:/qml/images/extra/openprivacy.png"
}
return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
}

+ 11
- 0
go/gobjects/contact.go View File

@@ -0,0 +1,11 @@
package gobjects

type Contact struct {
Handle string
DisplayName string
Image string
Server string
Badge int
Status int
Trusted bool
}

+ 8
- 0
go/gobjects/letter.go View File

@@ -0,0 +1,8 @@
package gobjects

// a Letter is a very simple message object passed to us from the UI
type Letter struct {
To, Message string
MID uint
}


+ 14
- 0
go/gobjects/message.go View File

@@ -0,0 +1,14 @@
package gobjects

import "time"

type Message struct {
Handle string
From string
DisplayName string
Message string
Image string
FromMe bool
MessageID int
Timestamp time.Time
}

+ 303
- 0
go/gothings/gcd.go View File

@@ -0,0 +1,303 @@
package gothings

import (
"bounce/go/constants"
"bounce/go/cwutil"

"encoding/base32"
"fmt"
"github.com/therecipe/qt/core"
"log"
"strings"
"time"
"bounce/go/the"
"cwtch.im/cwtch/model"
"bounce/go/characters"
"bounce/go/gobjects"
)

type GrandCentralDispatcher struct {
core.QObject

OutgoingMessages chan gobjects.Letter
UIState InterfaceState

_ string `property:"currentOpenConversation"`
_ float32 `property:"themeScale"`

// contact list stuff
_ func(handle, displayName, image, server string, badge, status int, trusted bool) `signal:"AddContact"`
_ func(handle, displayName, image, server string, badge, status int, trusted bool) `signal:"UpdateContact"`

// messages pane stuff
_ func(handle, from, displayName, message, image string, mID uint, fromMe bool, ts string) `signal:"AppendMessage"`
_ func() `signal:"ClearMessages"`
_ func() `signal:"ResetMessagePane"`
_ func(mID uint) `signal:"Acknowledged"`

// profile-area stuff
_ func(name, onion, image string) `signal:"UpdateMyProfile"`
_ func(status int, str string) `signal:"TorStatus"`

// other stuff i can't ontologize atm
_ func(str string) `signal:"InvokePopup"`
_ func(name, server, invitation string) `signal:"SupplyGroupSettings"`

// signals emitted from the ui (written in go, below)
_ func(message string, mid uint) `signal:"sendMessage,auto"`
_ func(onion string) `signal:"loadMessagesPane,auto"`
_ func(signal string) `signal:"broadcast,auto"` // convenience relay signal
_ func(str string) `signal:"importString,auto"`
_ func(str string) `signal:"popup,auto"`
_ func(nick string) `signal:"updateNick,auto"`
_ func(server, groupName string) `signal:"createGroup,auto"`
_ func() `signal:"requestGroupSettings,auto"`
}

func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) {
if len(message) > 65530 {
this.InvokePopup("message is too long")
return
}

if this.CurrentOpenConversation() == "" {
this.InvokePopup("ui error")
return
}

if len(this.CurrentOpenConversation()) == 32 { // SEND TO GROUP
if !the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted {
the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted = true
the.CwtchApp.SaveProfile(the.Peer)
c := this.UIState.GetContact(this.CurrentOpenConversation())
c.Trusted = true
this.UIState.UpdateContact(c.Handle)
}

the.Peer.SendMessageToGroup(this.CurrentOpenConversation(), message)
return
}

// TODO: require explicit invite accept/reject instead of implicitly trusting on send
if !this.UIState.GetContact(this.CurrentOpenConversation()).Trusted {
this.UIState.GetContact(this.CurrentOpenConversation()).Trusted = true
this.UIState.UpdateContact(this.CurrentOpenConversation())
}

select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages
// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
case this.OutgoingMessages <- gobjects.Letter{this.CurrentOpenConversation(), message, mID}:
default:
}

this.UIState.AddMessage(&gobjects.Message{
this.CurrentOpenConversation(),
"me",
"",
message,
"",
true,
int(mID),
time.Now(),
})
}

func (this *GrandCentralDispatcher) loadMessagesPane(handle string) {
this.ClearMessages()
this.SetCurrentOpenConversation(handle)
c := this.UIState.GetContact(handle)
c.Badge = 0
this.UIState.UpdateContact(handle)

if len(handle) == 32 { // LOAD GROUP
log.Printf("LOADING GROUP %s", handle)
tl := the.Peer.GetGroup(handle).GetTimeline()
log.Printf("messages: %d", len(tl))
for i := range tl {
if tl[i].PeerID == the.Peer.GetProfile().Onion {
handle = "me"
} else {
handle = tl[i].PeerID
}
var name string
var exists bool
name, exists = the.Peer.GetProfile().GetCustomAttribute(tl[i].PeerID + "_name")
if !exists || name == "" {
name = tl[i].PeerID[:16] + "..."
}
this.AppendMessage(
handle,
tl[i].PeerID,
name,
tl[i].Message,
cwutil.RandomProfileImage(tl[i].PeerID),
0,
tl[i].PeerID == the.Peer.GetProfile().Onion,
tl[i].Timestamp.Format(constants.TIME_FORMAT),
)
}
return
} // ELSE LOAD CONTACT

messages := this.UIState.GetMessages(handle)
for i := range messages {
from := messages[i].From
if messages[i].FromMe {
from = "me"
}
this.AppendMessage(
messages[i].Handle,
from,
messages[i].DisplayName,
messages[i].Message,
cwutil.RandomProfileImage(handle),
uint(messages[i].MessageID),
messages[i].FromMe,
messages[i].Timestamp.Format(constants.TIME_FORMAT),
)
}
}

func (this *GrandCentralDispatcher) requestGroupSettings() {
log.Printf("requestGroupSettings()")
group := the.Peer.GetGroup(this.CurrentOpenConversation())
nick, _ := group.GetAttribute("nick")
invite, _ := the.Peer.ExportGroup(this.CurrentOpenConversation())
this.SupplyGroupSettings(nick, group.GroupServer, invite)
}

func (this *GrandCentralDispatcher) broadcast(signal string) {
switch signal {
default:
log.Printf("unhandled broadcast signal: %v", signal)
case "ResetMessagePane":
this.ResetMessagePane()
}
}

func (this *GrandCentralDispatcher) importString(str string) {
if len(str) < 5 {
log.Printf("ignoring short string")
return
}

log.Printf("importing: %s\n", str)
onion := str
name := onion
str = strings.TrimSpace(str)

//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
if str[0:5] == "torv3" { // GROUP INVITE
groupID, err := the.Peer.ImportGroup(str)
if err != nil {
this.InvokePopup("not a valid group invite")
return
}

group := the.Peer.GetGroup(groupID)
the.Peer.JoinServer(group.GroupServer)
the.CwtchApp.SaveProfile(the.Peer)
this.UIState.AddContact(&gobjects.Contact{
groupID[:12],
groupID,
cwutil.RandomGroupImage(groupID),
group.GroupServer,
0,
0,
true,
})
fmt.Printf("imported groupid=%s server=%s", groupID, group.GroupServer)

return
}

if strings.Contains(str, " ") { // usually people prepend spaces and we don't want it going into the name (use ~ for that)
parts := strings.Split(strings.TrimSpace(str), " ")
str = parts[len(parts)-1]
}

if strings.Contains(str, "~") {
parts := strings.Split(str, "~")
onion = parts[len(parts)-1]
name = strings.Join(parts[:len(parts)-1], " ")
}

if len(onion) != 56 {
this.InvokePopup("invalid format")
return
}

name = strings.TrimSpace(name)
if name == "" {
this.InvokePopup("empty name")
return
}

if len(name) > 32 {
name = name[:32] //TODO: better strategy for long names?
}

_, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
if err != nil {
log.Printf("%v", err)
this.InvokePopup("bad format. missing characters?")
return
}

_, exists := the.Peer.GetProfile().GetCustomAttribute(name + "_onion")
if exists {
this.InvokePopup("can't re-use names :(")
return
}

_, exists = the.Peer.GetProfile().GetCustomAttribute(onion + "_name")
if exists {
this.InvokePopup("already have this contact")
return //TODO: bring them to the duplicate
}

this.UIState.AddContact(&gobjects.Contact{
onion,
name,
cwutil.RandomProfileImage(onion),
"",
0,
0,
true,
})
}

func (this *GrandCentralDispatcher) popup(str string) {
this.InvokePopup(str)
}

func (this *GrandCentralDispatcher) updateNick(nick string) {
the.Peer.GetProfile().Name = nick
the.CwtchApp.SaveProfile(the.Peer)
}

func (this *GrandCentralDispatcher) createGroup(server, groupName string) {
groupID, _, err := the.Peer.StartGroup(server)
if err != nil {
this.popup("group creation failed :(")
return
}

this.UIState.AddContact(&gobjects.Contact{
groupID,
groupName,
cwutil.RandomGroupImage(groupID),
server,
0,
0,
true,
})

group := the.Peer.GetGroup(groupID)
group.SetAttribute("nick", groupName)
the.CwtchApp.SaveProfile(the.Peer)

the.Peer.JoinServer(server)
group.NewMessage = make(chan model.Message)
go characters.CwtchListener(this.UIState.AddMessage, group.GroupID, group.NewMessage)
}

+ 116
- 0
go/gothings/uistate.go View File

@@ -0,0 +1,116 @@
package gothings

import (
"cwtch.im/cwtch/model"
"encoding/base32"
"strings"
"log"
"bounce/go/constants"
"bounce/go/the"
"bounce/go/gobjects"
)

type InterfaceState struct {
parentGcd *GrandCentralDispatcher
contacts map[string]*gobjects.Contact
messages map[string][]*gobjects.Message
}

func NewUIState(gcd *GrandCentralDispatcher) (uis InterfaceState) {
uis = InterfaceState{gcd, make(map[string]*gobjects.Contact), make(map[string][]*gobjects.Message)}
return
}

func (this *InterfaceState) AddContact(c *gobjects.Contact) {
if len(c.Handle) == 32 { // ADD GROUP
//TODO: we should handle group creation here too probably? the code for groups vs individuals is weird right now ^ea

if _, found := this.contacts[c.Handle]; !found {
this.contacts[c.Handle] = c
this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
}

return
} else if len(c.Handle) != 56 {
log.Printf("sorry, unable to handle AddContact(%v)", c.Handle)
return
}

if the.Peer.GetContact(c.Handle) == nil {
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(c.Handle))

pp := &model.PublicProfile{Name: c.DisplayName, Ed25519PublicKey: decodedPub[:32], Trusted: c.Trusted, Blocked:false, Onion:c.Handle, Attributes:make(map[string]string)}
pp.SetAttribute("name", c.DisplayName)
the.Peer.GetProfile().Contacts[c.Handle] = pp
if c.Trusted {
the.Peer.GetProfile().TrustPeer(c.Handle)
}
the.CwtchApp.SaveProfile(the.Peer)
go the.Peer.PeerWithOnion(c.Handle)
}

if _, found := this.contacts[c.Handle]; !found {
this.contacts[c.Handle] = c
this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
}

the.CwtchApp.SaveProfile(the.Peer)
}

func (this *InterfaceState) GetContact(handle string) *gobjects.Contact {
return this.contacts[handle]
}

func (this *InterfaceState) AddMessage(m *gobjects.Message) {
_, found := this.contacts[m.Handle]
if !found {
this.AddContact(&gobjects.Contact{
m.DisplayName,
m.Image,
m.Handle,
"",
1,
0,
false,
})
}

c := the.Peer.GetContact(m.Handle)
if c == nil {

}

_, found = this.messages[m.Handle]
if !found {
this.messages[m.Handle] = make([]*gobjects.Message, 0)
}

this.messages[m.Handle] = append(this.messages[m.Handle], m)

if this.parentGcd.CurrentOpenConversation() == m.Handle {
if m.FromMe {
m.From = "me"
}

this.parentGcd.AppendMessage(m.Handle, m.From, m.DisplayName, m.Message, m.Image, uint(m.MessageID), m.FromMe, m.Timestamp.Format(constants.TIME_FORMAT))
} else {
c := this.GetContact(m.Handle)
c.Badge++
this.UpdateContact(c.Handle)
}
}

func (this *InterfaceState) GetMessages(handle string) []*gobjects.Message {
_, found := this.messages[handle]
if !found {
this.messages[handle] = make([]*gobjects.Message, 0)
}
return this.messages[handle]
}

func (this *InterfaceState) UpdateContact(handle string) {
c, found := this.contacts[handle]
if found {
this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
}
}

+ 11
- 0
go/the/globals.go View File

@@ -0,0 +1,11 @@
package the

import (
libPeer "cwtch.im/cwtch/peer"
"cwtch.im/cwtch/app"
)

var CwtchApp app.Application
var Peer libPeer.CwtchPeer
var CwtchDir string
var AcknowledgementIDs map[uint32]uint

+ 106
- 274
main.go View File

@@ -1,84 +1,73 @@
package main

import (
libpeer "cwtch.im/cwtch/peer"
"cwtch.im/cwtch/peer/connections"
"encoding/base32"
"fmt"
"github.com/sethvargo/go-diceware/diceware"
"github.com/therecipe/qt/core"
"bounce/go/characters"
"bounce/go/cwutil"
"bounce/go/the"
libapp "cwtch.im/cwtch/app"
"cwtch.im/cwtch/model"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/quick"
"github.com/therecipe/qt/quickcontrols2"
"github.com/therecipe/qt/widgets"
"log"
"os"
"os/user"
"path"
"strings"
"time"
"strconv"
"git.openprivacy.ca/openprivacy/asaur"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"log"
"cwtch.im/cwtch/model"
"encoding/hex"
"cwtch.im/cwtch/app"
"bounce/go/cwtchthings"
"bounce/go/gothings"
"bounce/go/gobjects"
)

var gcd *GrandCentralDispatcher

type ContactManager map[string]*Contact

var contactMgr ContactManager
var peer libpeer.CwtchPeer
var outgoingMessages chan Message

// need this to translate from the message IDs the UI uses to the ones the acknowledgement system uses on the wire
var acknowledgementIDs map[uint32]uint

type Contact struct {
Messages []Message
Unread int
Status connections.ConnectionState
func init() {
// make go-defined types available in qml
gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
}

func (this *Contact) AddMessage(m Message) {
this.Messages = append(this.Messages, m)
}
func main() {
// our globals
gcd := gothings.NewGrandCentralDispatcher(nil)
gcd.UIState = gothings.NewUIState(gcd)
the.AcknowledgementIDs = make(map[uint32]uint)
gcd.OutgoingMessages = make(chan gobjects.Letter, 1000)

//TODO: put theme stuff somewhere better
gcd.SetThemeScale(1.0)

// this is to load local qml files quickly when developing
var qmlSource *core.QUrl
if len(os.Args) == 2 && os.Args[1] == "-local" {
qmlSource = core.QUrl_FromLocalFile("./qml/main.qml")
} else {
qmlSource = core.NewQUrl3("qrc:/qml/main.qml", 0)
}

type Message struct {
With, Message string
FromMe bool
MessageID uint
Timestamp time.Time
}
// window construction boilerplate
view := initializeQtView()

func DeliverMessageToUI(handle, from, displayname, message string, mID uint, fromMe bool, ts time.Time) {
_, found := contactMgr[handle]
if !found {
contactMgr[handle] = &Contact{[]Message{}, 0, 0}
}
// variables we want to access from inside qml
view.RootContext().SetContextProperty("gcd", gcd)

contactMgr[handle].AddMessage(Message{from, message, fromMe, mID, ts})
if gcd.currentOpenConversation == handle {
if fromMe {
from = "me"
}
gcd.AppendMessage(from, message, displayname, mID, ts.Format(TIME_FORMAT), randomProfileImage(from))
} else {
contactMgr[handle].Unread++
gcd.SetUnread(handle, contactMgr[handle].Unread)
}
}
// this actually loads the qml
view.SetSource(qmlSource)

func init() {
GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
}
// these are long-lived pollers/listeners for incoming messages and status changes
loadCwtchData(gcd)
go characters.PostmanPat(gcd.OutgoingMessages)
go characters.TorStatusPoller(gcd.TorStatus)
go characters.PresencePoller(gcd.UIState.GetContact, gcd.UIState.AddContact, gcd.UIState.UpdateContact)
go characters.GroupPoller(gcd.UIState.GetContact, gcd.UIState.UpdateContact)

func main() {
contactMgr = make(ContactManager)
acknowledgementIDs = make(map[uint32]uint)
// here we go!
view.Show()
widgets.QApplication_Exec()
}

// window construction boilerplate
func initializeQtView() *quick.QQuickView {
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true)
widgets.NewQApplication(len(os.Args), os.Args)
quickcontrols2.QQuickStyle_SetStyle("Universe")
@@ -87,176 +76,23 @@ func main() {
view.SetMinimumHeight(280)
view.SetMinimumWidth(300)
view.SetTitle("cwtch")
gcd = NewGrandCentralDispatcher(nil)
view.RootContext().SetContextProperty("gcd", gcd)

if len(os.Args) == 2 && os.Args[1] == "local" {
view.SetSource(core.QUrl_FromLocalFile("./qml/main.qml"))
} else {
view.SetSource(core.NewQUrl3("qrc:/qml/main.qml", 0))
}

outgoingMessages = make(chan Message, 1000)
go postmanPat()

initialize(view)
view.Show()
go torStatusPoller()
go presencePoller()
go groupPoller()
go ricochetListener()
widgets.QApplication_Exec()
}

func groupPoller() {
for {
time.Sleep(time.Second * 4)

servers := peer.GetServers()
groups := peer.GetGroups()
for i := range groups {
group := peer.GetGroup(groups[i])
//log.Printf("setting group %s to status %d", groups[i], int(servers[group.GroupServer]))
gcd.SetConnectionStatus(groups[i], int(servers[group.GroupServer]))
}
}
}

func presencePoller() { // TODO: make this subscribe-able in ricochet
time.Sleep(time.Second * 4)
for {
contacts := peer.GetContacts()
for i := range contacts {
_, found := contactMgr[contacts[i]]
if !found { // new contact has attempted to connect with us, treat it as an invite
contactMgr[contacts[i]] = &Contact{[]Message{}, 0, -1}
c, _ := peer.GetProfile().GetContact(contacts[i])
peer.GetProfile().SetCustomAttribute(contacts[i]+"_name", c.Name)
peer.GetProfile().SetCustomAttribute(c.Name+"_onion", contacts[i])
peer.Save()
gcd.AddContact(c.Name, contacts[i], "", randomProfileImage(contacts[i]), "0", c.Trusted)
}

c, found := peer.GetPeers()[contacts[i]]
if !found && contactMgr[contacts[i]].Status != -2 {
//log.Printf("setting %v to -2", contacts[i])
contactMgr[contacts[i]].Status = -2
gcd.SetConnectionStatus(contacts[i], -2)
} else if contactMgr[contacts[i]].Status != c {
//log.Printf("was: %v", contactMgr[contacts[i]].Status)
contactMgr[contacts[i]].Status = c
//log.Printf("setting %v to status %v", contacts[i], c)
gcd.SetConnectionStatus(contacts[i], int(c))
//log.Printf("now: %v", contactMgr[contacts[i]].Status)
}
}
time.Sleep(time.Second * 4)
}
}

func torStatusPoller() {
for {
time.Sleep(time.Second)
//todo: this should use a config manager
//todo: also, try dialing the proxy to differentiate tor not running vs control port not configured
rawStatus, err := asaur.GetInfo("localhost:9051", "tcp4", "", "status/bootstrap-phase")
if err != nil {
gcd.TorStatus(0, "can't find tor. is it running? is the controlport configured?")
continue
}

status := asaur.ParseBootstrapPhase(rawStatus)
progress, _ := strconv.Atoi(status["PROGRESS"])

if status["TAG"] == "done" {
gcd.TorStatus(3, "tor appears to be running just fine!")
continue
}

if progress == 0 {
gcd.TorStatus(1, "tor is trying to start up")
continue
}

gcd.TorStatus(2, status["SUMMARY"])
}
}

func cwtchListener(groupID string, channel chan model.Message) {
for {
m := <-channel
log.Printf("GROUPMSG %s %s", m.Message, m.PeerID)
name := m.PeerID
if name == peer.GetProfile().Onion {
name = "me"
} else {
var exists bool // lol this is a golang antifeature
name, exists = peer.GetProfile().GetCustomAttribute(m.PeerID + "_name")
if !exists {
name = ""
}
}
if name == "" {
name = m.PeerID[:16] + "..."
}
DeliverMessageToUI(groupID, m.PeerID, name, m.Message, 0, m.PeerID == peer.GetProfile().Onion, m.Timestamp)
peer.Save()
}
}

func ricochetListener() {
err := peer.Listen()
if err != nil {
fmt.Printf("error listening for connections: %v\n", err)
gcd.InvokePopup("error handling network connection")
}
}

func postmanPat() {
postOffice := make(map[string]chan Message)

for {
m := <-outgoingMessages

_, found := postOffice[m.With]
if !found {
postOffice[m.With] = make(chan Message, 100)
go andHisBlackAndWhiteCat(postOffice[m.With])
}

postOffice[m.With] <- m
}
}

func andHisBlackAndWhiteCat(incomingMessages chan Message) {
for {
m := <-incomingMessages
connection := peer.PeerWithOnion(m.With)
connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) {
chatchannel, ok := channel.Handler.(*channels.ChatChannel)
if ok {
log.Printf("Sending packet")
acknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MessageID
}
})
}
return view
}

func initialize(view *quick.QQuickView) {
// this is mostly going to get factored out when we add profile support
// for now, it loads a single peer and fills the ui with its data
func loadCwtchData(gcd *gothings.GrandCentralDispatcher) {
var err error
//TODO: this section is ported over and has a lot of printf errors, need to show them in the ui
var dirname, filename string
if os.Getenv("CWTCH_FOLDER") != "" {
dirname = os.Getenv("CWTCH_FOLDER")
filename = path.Join(dirname, "keep-this-file-private")
the.CwtchDir = os.Getenv("CWTCH_FOLDER")
} else {
usr, err := user.Current()
if err != nil {
fmt.Printf("\nerror: could not load current user: %v\n", err)
os.Exit(1)
}
dirname = path.Join(usr.HomeDir, ".cwtch")
filename = path.Join(dirname, "keep-this-file-private")
the.CwtchDir = path.Join(usr.HomeDir, ".cwtch")
}

/*_, err := app2.NewApp(dirname, "/data/data/org.qtproject.example.go/lib/libtor.so")
@@ -265,83 +101,79 @@ func initialize(view *quick.QQuickView) {
}
time.Sleep(time.Second * 10)
*/
os.MkdirAll(dirname, 0700)
os.MkdirAll(the.CwtchDir, 0700)

_, err = app.NewApp("/data/data/org.qtproject.example.go/files/", "/data/data/org.qtproject.example.go/lib/libtor.so")
log.Printf("starting Swtch app: err: %v\n", err)
the.CwtchApp, err = libapp.NewApp(the.CwtchDir, "tor")
if err != nil {
//TODO no more fatalfs
log.Fatalf("couldn't create cwtch app: %v", err)
}

peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime")
err = the.CwtchApp.LoadProfiles("be gay do crime")
if err != nil {
fmt.Println("couldn't load your config file, attempting to create a new one now")
//TODO no more fatalfs
log.Fatalf("couldn't load profiles: %v", err)
}

names, err := diceware.Generate(1)
peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename)
if len(the.CwtchApp.ListPeers()) == 0 {
log.Printf("couldn't load your config file. attempting to create one now")
the.Peer, err = the.CwtchApp.CreatePeer("alice", "be gay do crime")
if err != nil {
fmt.Println("couldn't create one either :( exiting")
os.Exit(1)
log.Fatalf("couldn't create one. is your cwtch folder writable?")
}

peer.Save()
the.CwtchApp.SaveProfile(the.Peer)
} else {
the.Peer = the.CwtchApp.PrimaryIdentity()
}

gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion))
gcd.UpdateMyProfile(the.Peer.GetProfile().Name, the.Peer.GetProfile().Onion, cwutil.RandomProfileImage(the.Peer.GetProfile().Onion))

aif := application.ApplicationInstanceFactory{}
aif.Init()
app := new(application.RicochetApplication)
aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
ccl := new(ChatChannelListener)
ccl.Init(rai, app)
ccl := new(cwtchthings.ChatChannelListener)
ccl.Init(rai, app, gcd.UIState.AddMessage, gcd.Acknowledged)
return func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = ccl
return chat
}
})
peer.SetApplicationInstanceFactory(aif)
the.Peer.SetApplicationInstanceFactory(aif)
the.CwtchApp.LaunchPeers()

contacts := peer.GetContacts()

contacts := the.Peer.GetContacts()
for i := range contacts {
attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name")
contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0}
gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted)
contact, _ := the.Peer.GetProfile().GetContact(contacts[i])
displayName, _ := contact.GetAttribute("name")
gcd.UIState.AddContact(&gobjects.Contact{
contacts[i],
displayName,
cwutil.RandomProfileImage(contacts[i]),
"",
0,
0,
contact.Trusted,
})
}

groups := peer.GetGroups()
groups := the.Peer.GetGroups()
for i := range groups {
group := peer.GetGroup(groups[i])
log.Printf("adding saved group %v", groups[i])
group := the.Peer.GetGroup(groups[i])
group.NewMessage = make(chan model.Message)
go cwtchListener(groups[i], group.NewMessage)
peer.JoinServer(group.GroupServer)
//TODO: base the profileImage off the groupid, not the server. probably by decoding it and then re-encoding it b32
gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted)
log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer)
}

}

// temporary until we do real picture selection
func randomProfileImage(onion string) string {
//TODO: this is a hack, fix ever passing this in
if onion == "me" {
onion = peer.GetProfile().Onion
}

choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"}
barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err != nil || len(barr) != 35 {
fmt.Printf("error: %v %v %v\n", onion, err, barr)
return "qrc:/qml/images/extra/openprivacy.png"
}
return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png"
}

func randomGroupImage(onion string) string {
choices := []string{"001-borobudur","002-opera-house","003-burj-al-arab","004-chrysler","005-acropolis","006-empire-state-building","007-temple","008-indonesia-1","009-new-zealand","010-notre-dame","011-space-needle","012-seoul","013-mosque","014-milan","015-statue","016-pyramid","017-cologne","018-brandenburg-gate","019-berlin-cathedral","020-hungarian-parliament","021-buckingham","022-thailand","023-independence","024-angkor-wat","025-vaticano","026-christ-the-redeemer","027-colosseum","028-golden-gate-bridge","029-sphinx","030-statue-of-liberty","031-cradle-of-humankind","032-istanbul","033-london-eye","034-sagrada-familia","035-tower-bridge","036-burj-khalifa","037-washington","038-big-ben","039-stonehenge","040-white-house","041-ahu-tongariki","042-capitol","043-eiffel-tower","044-church-of-the-savior-on-spilled-blood","045-arc-de-triomphe","046-windmill","047-louvre","048-torii-gate","049-petronas","050-matsumoto-castle","051-fuji","052-temple-of-heaven","053-pagoda","054-chichen-itza","055-forbidden-city","056-merlion","057-great-wall-of-china","058-taj-mahal","059-pisa","060-indonesia"}
barr, err := hex.DecodeString(onion)
if err != nil || len(barr) == 0 {
fmt.Printf("error: %v %v %v\n", onion, err, barr)
return "qrc:/qml/images/extra/openprivacy.png"
go characters.CwtchListener(gcd.UIState.AddMessage, groups[i], group.NewMessage)
the.Peer.JoinServer(group.GroupServer)
gcd.UIState.AddContact(&gobjects.Contact{
group.GroupID,
group.GroupID[:12],
cwutil.RandomGroupImage(group.GroupID),
group.GroupServer,
0,
0,
group.Accepted,
})
}
return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
}

+ 5
- 55
qml/main.qml View File

@@ -6,6 +6,7 @@ import QtQuick.Layouts 1.3
import QtQuick.Window 2.11

import "fonts/Twemoji.js" as T
import "panes"
import "widgets"

Item {
@@ -133,6 +134,7 @@ Item {
readonly property int settingsPane: 2
readonly property int userProfilePane: 3
readonly property int groupProfilePane: 4
readonly property int addGroupPane: 5


Item {} // empty
@@ -141,47 +143,7 @@ Item {
anchors.fill: parent
}

ColumnLayout { // settingsPane
anchors.fill: parent


StackToolbar {
text: "Cwtch Settings"
aux.visible: false
}

ScalingLabel {
Layout.maximumWidth: parent.width
text: "welcome to the global app settings page!"
}

Slider {
id: zoomSlider
from: 0.5
to: 2.4
}

ScalingLabel {
text: "Large text"
size: 20
}

ScalingLabel{
text: "Default size text, scale factor: " + zoomSlider.value
}

ScalingLabel {
text: "Small text"
size: 8
}


Component.onCompleted: {
zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
}
}
SettingsPane{}

ColumnLayout { // userProfilePane
anchors.fill: parent
@@ -197,21 +159,9 @@ Item {
Label { text: "per-user things like contact name and picture will be edited here" }
}

Label { // groupProfilePane
font.pixelSize: 12
text: "invite new people or change the group name here"
GroupSettingsPane{}


StackToolbar { text: "Group settings" }
}

Label { // addGroupPane
font.pixelSize: 12
text: "add a new group"


StackToolbar { text: "Create group" }
}
AddGroupPane { anchors.fill: parent }
}
}
}


+ 53
- 0
qml/panes/AddGroupPane.qml View File

@@ -0,0 +1,53 @@
import QtGraphicalEffects 1.0
import QtQuick 2.7
import QtQuick.Controls 2.4
import QtQuick.Controls.Material 2.0
import QtQuick.Layouts 1.3
import QtQuick.Window 2.11

import "../widgets"

ColumnLayout { // settingsPane
anchors.fill: parent


StackToolbar {
text: "Create a new group"
aux.visible: false
}

ScalingLabel {
text: "Server:"
}

TextEdit {
id: txtServer
width: 100
text: "6xnl4rlc5gq5crnyki6wp6qy2dy2brsjtfixwhgujfycsu7jly7pmxad"
}

ScalingLabel{
text: "Group name:"
}

TextEdit {
id: txtGroupName
width: 100
text: "my awesome group"
}

SimpleButton {
text: "create"

onClicked: {
gcd.createGroup(txtServer.text, txtGroupName.text)
}
}


Component.onCompleted: {
zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
}
}

+ 68
- 0
qml/panes/GroupSettingsPane.qml View File

@@ -0,0 +1,68 @@
import QtGraphicalEffects 1.0
import QtQuick 2.7
import QtQuick.Controls 2.4
import QtQuick.Controls.Material 2.0
import QtQuick.Layouts 1.3
import QtQuick.Window 2.11

import "../widgets"

ColumnLayout { // groupSettingsPane
anchors.fill: parent


StackToolbar {
id: toolbar
aux.visible: false
}

ScalingLabel {
text: "Server:"
}

TextEdit {
id: txtServer
width: 100
}

ScalingLabel{
text: "Group name:"
}

TextEdit {
id: txtGroupName
width: 100
}

ScalingLabel {
text: "Invitation:"
}

TextEdit {
id: txtInvitation
width: 200
}

SimpleButton {
icon: "regular/clipboard"
text: "copy"

onClicked: {
gcd.popup("copied to clipboard!")
txtInvitation.selectAll()
txtInvitation.copy()
}
}


Connections {
target: gcd

onSupplyGroupSettings: function(name, server, invite) {
toolbar.text = name
txtGroupName.text = name
txtServer.text = server
txtInvitation.text = invite
}
}
}

+ 50
- 0
qml/panes/SettingsPane.qml View File

@@ -0,0 +1,50 @@
import QtGraphicalEffects 1.0
import QtQuick 2.7
import QtQuick.Controls 2.4
import QtQuick.Controls.Material 2.0
import QtQuick.Layouts 1.3
import QtQuick.Window 2.11

import "../widgets"

ColumnLayout { // settingsPane
anchors.fill: parent


StackToolbar {
text: "Cwtch Settings"
aux.visible: false
}

ScalingLabel {
Layout.maximumWidth: parent.width
text: "welcome to the global app settings page!"
}

Slider {
id: zoomSlider
from: 0.5
to: 2.4
}

ScalingLabel {
text: "Large text"
size: 20
}

ScalingLabel{
text: "Default size text, scale factor: " + zoomSlider.value
}

ScalingLabel {
text: "Small text"
size: 8
}


Component.onCompleted: {
zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
}
}

+ 18
- 18
qml/widgets/ContactList.qml View File

@@ -8,11 +8,11 @@ ColumnLayout {
id: root


MyProfile{
MyProfile{ // CURRENT PROFILE INFO AND CONTROL BAR
id: myprof
}

Flickable { // CONTACT LIST
Flickable { // THE ACTUAL CONTACT LIST
id: sv
//Layout.alignment: Qt.AlignLeft | Qt.AlignTop
clip: true
@@ -36,18 +36,18 @@ ColumnLayout {
width: root.width
spacing: 0


Connections { // ADD/REMOVE CONTACT ENTRIES
target: gcd

onAddContact: function(name, onion, server, image, badge, trusted) {
onAddContact: function(handle, displayName, image, server, badge, status, trusted) {
contactsModel.append({
"n": name,
"o": onion,
"s": server,
"i": image,
"b": badge,
"t": trusted
"_handle": handle,
"_displayName": displayName,
"_image": image,
"_server": server,
"_badge": badge,
"_status": status,
"_trusted": trusted,
})
}
}
@@ -56,16 +56,16 @@ ColumnLayout {
id: contactsModel
}


Repeater {
model: contactsModel // ... AND DISPLAYED HERE
delegate: Contact{
nick: n
onion: o
server: s
image: i
badge: b
isTrusted: t
delegate: ContactRow {
handle: _handle
displayName: _displayName
image: _image
server: _server
badge: _badge
status: _status
trusted: _trusted
}
}
}


qml/widgets/Contact.qml → qml/widgets/ContactRow.qml View File

@@ -8,15 +8,15 @@ import CustomQmlTypes 1.0
RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
anchors.left: parent.left
anchors.right: parent.right
visible: nick != ""
visible: true

property alias nick: cn.text
property alias displayName: cn.text
property alias image: imgProfile.source
property string onion
property string badge
property string handle
property int badge
property bool isActive
property bool isHover
property bool isTrusted
property bool trusted
property alias status: imgProfile.status
property string server

@@ -48,7 +48,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
anchors.left: imgProfile.right
anchors.right: rectUnread.left
font.pixelSize: 16
font.italic: !isTrusted
font.italic: !trusted
textFormat: Text.PlainText
//fontSizeMode: Text.HorizontalFit
elide: Text.ElideRight
@@ -61,7 +61,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
width: lblUnread.width + 10
radius: 8
color: "#4B3557"
visible: badge != "0"
visible: badge != 0
anchors.rightMargin: 9


@@ -85,7 +85,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
gcd.broadcast("ResetMessagePane")
isActive = true
theStack.pane = theStack.messagePane
gcd.loadMessagesPane(onion)
gcd.loadMessagesPane(handle)
}

onEntered: {
@@ -100,25 +100,18 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
Connections { // UPDATE UNREAD MESSAGES COUNTER
target: gcd

onSetUnread: function(foronion, n) {
if (onion == foronion) {
badge = ""+n
}
}

onResetMessagePane: function() {
isActive = false
}

onSetConnectionStatus: function(foronion, x) {
if (onion == foronion) {
status = x
}
}

onMarkTrusted: function(foronion) {
if (onion == foronion) {
isTrusted = true
onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted) {
if (handle == _handle) {
displayName = _displayName
image = _image
server = _server
badge = _badge
status = _status
trusted = _trusted
}
}
}

+ 6
- 4
qml/widgets/Message.qml View File

@@ -15,10 +15,12 @@ RowLayout {

property alias message: lbl.text
property string from
property string displayname
property string handle
property string displayName
property int messageID
property bool fromMe
property alias timestamp: ts.text
property alias source: imgProfile.source
property alias image: imgProfile.source
property alias status: imgProfile.status


@@ -107,14 +109,14 @@ RowLayout {
color: "#FFFFFF"
font.pixelSize: 10
anchors.right: parent.right
text: displayname
text: displayName
visible: from != "me"
}

Image { // ACKNOWLEDGEMENT ICON
id: ack
anchors.right: parent.right
source: displayname == "" ? "qrc:/qml/images/fontawesome/regular/hourglass.svg" : "qrc:/qml/images/fontawesome/regular/check-circle.svg"
source: displayName == "" ? "qrc:/qml/images/fontawesome/regular/hourglass.svg" : "qrc:/qml/images/fontawesome/regular/check-circle.svg"
height: 10
sourceSize.height: 10
visible: from == "me"


+ 23
- 16
qml/widgets/MessageList.qml View File

@@ -14,7 +14,10 @@ ColumnLayout {
StackToolbar {
text: "open privacy exec"

aux.onClicked: theStack.pane = theStack.userProfilePane
aux.onClicked: {
theStack.pane = gcd.currentOpenConversation.length == 32 ? theStack.groupProfilePane : theStack.userProfilePane
gcd.requestGroupSettings()
}
}

Flickable { // THE MESSAGE LIST ITSELF
@@ -38,14 +41,16 @@ ColumnLayout {
messagesModel.clear()
}

onAppendMessage: function(from, message, displayname, mid, ts, source) {
onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts) {
messagesModel.append({
"f": from,
"m": parse(message, 12),
"d": displayname,
"i": mid,
"t": ts,
"src": source
"_handle": handle,
"_from": from,
"_displayName": displayName,
"_message": parse(message, 12),
"_image": image,
"_mid": mid,
"_fromMe": fromMe,
"_ts": ts,
})

if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) {
@@ -73,12 +78,14 @@ ColumnLayout {
Repeater { // ... AND DISPLAYED HERE
model: messagesModel
delegate: Message {
from: f
message: m
displayname: d
messageID: i
timestamp: t
source: src
handle: _handle
from: _from
displayName: _displayName
message: _message
image: _image
messageID: _mid
fromMe: _fromMe
timestamp: _ts
}
}
}
@@ -88,8 +95,8 @@ ColumnLayout {
Rectangle { // MESSAGE ENTRY TEXTFIELD
id: rectMessage
Layout.fillWidth: true
Layout.minimumHeight: 40 * zoomSlider.value
Layout.maximumHeight: 40 * zoomSlider.value
Layout.minimumHeight: 40 * gcd.themeScale
Layout.maximumHeight: 40 * gcd.themeScale
color: "#EDEDED"
border.color: "#AAAAAA"
radius: 10


+ 16
- 3
qml/widgets/MyProfile.qml View File

@@ -149,7 +149,7 @@ ColumnLayout {

Row { // TOOLS FOR EDITING PROFILE
anchors.horizontalCenter: parent.horizontalCenter
spacing: zoomSlider.value * 2
spacing: gcd.themeScale * 2


TextEdit { // USED TO POWER THE COPY/PASTE BUTTON
@@ -164,8 +164,8 @@ ColumnLayout {
onClicked: {
gcd.popup("copied to clipboard!")
txtHidden.text = nick.replace(" ", "~") + "~" + onion
txtHidden.selectAll();
txtHidden.copy();
txtHidden.selectAll()
txtHidden.copy()
}
}

@@ -184,6 +184,19 @@ ColumnLayout {
}
}

Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: gcd.themeScale * 2


SimpleButton { // CREATE GROUP BUTTON
icon: "regular/clipboard"
text: "new group"

onClicked: theStack.pane = theStack.addGroupPane
}
}

RowLayout {
anchors.left: parent.left
anchors.right: parent.right


BIN
qml/widgets/MyProfile.qmlc View File


+ 1
- 1
qml/widgets/ScalingLabel.qml View File

@@ -7,7 +7,7 @@ import QtQuick.Window 2.11


Label {
font.pixelSize: zoomSlider.value * size
font.pixelSize: gcd.themeScale * size
wrapMode: Text.WordWrap

property real size: 12

+ 2
- 2
qml/widgets/SimpleButton.qml View File

@@ -9,10 +9,10 @@ import "../fonts/Twemoji.js" as T

Rectangle {
id: button
width: (text == undefined || text == "" ? 0 : buttonText.width) + (icon == undefined || icon == "" ? 0 : ico.width) + 24 * zoomSlider.value
width: (text == undefined || text == "" ? 0 : buttonText.width) + (icon == undefined || icon == "" ? 0 : ico.width) + 24 * gcd.themeScale
Layout.minimumWidth: width
Layout.maximumWidth: width
height: 20 * zoomSlider.value
height: 20 * gcd.themeScale
Layout.minimumHeight: height
Layout.maximumHeight: height
color: mousedown ? "#B09CBC" : "#4B3557"


BIN
qml/widgets/SimpleButton.qmlc View File


+ 1
- 1
qml/widgets/StackToolbar.qml View File

@@ -12,7 +12,7 @@ Rectangle { // OVERHEAD BAR ON STACK PANE
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 20 * zoomSlider.value + 4
height: 20 * gcd.themeScale + 4
Layout.minimumHeight: height
Layout.maximumHeight: height
color: "#EDEDED"


Loading…
Cancel
Save