initial commit

This commit is contained in:
erinn 2018-11-21 16:01:17 -08:00
parent 690a71308e
commit 8a9ba6d154
30 changed files with 1074 additions and 650 deletions

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()
}

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,
})
}
}

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)
}
}
}

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
}
})
}
}

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)
}
}

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
go/constants/style.go Normal file
View File

@ -0,0 +1,3 @@
package constants
var TIME_FORMAT = "Mon 3:04pm"

View File

@ -1,20 +1,26 @@
package main package cwtchthings
import ( import (
"git.openprivacy.ca/openprivacy/libricochet-go/application" "git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels" "git.openprivacy.ca/openprivacy/libricochet-go/channels"
"time" "time"
) "bounce/go/the"
"bounce/go/cwutil"
"bounce/go/gobjects"
)
type ChatChannelListener struct { type ChatChannelListener struct {
rai *application.ApplicationInstance rai *application.ApplicationInstance
ra *application.RicochetApplication ra *application.RicochetApplication
addMessage func(*gobjects.Message)
acknowledged func(uint)
} }
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, addMessage func(*gobjects.Message), acknowledged func(uint)) {
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
this.rai = rai this.rai = rai
this.ra = ra this.ra = ra
this.addMessage = addMessage
this.acknowledged = acknowledged
} }
// We always want bidirectional chat channels // 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 { func (this *ChatChannelListener) ChatMessage(messageID uint32, when time.Time, message string) bool {
DeliverMessageToUI(this.rai.RemoteHostname, this.rai.RemoteHostname, "", message, uint(messageID), false, when) this.addMessage(&gobjects.Message{
go func() { 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) time.Sleep(time.Second)
peer.Save() the.CwtchApp.SaveProfile(the.Peer)
}() }()
return true return true
} }
func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) { func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) {
gcd.Acknowledged(acknowledgementIDs[messageID]) this.acknowledged(the.AcknowledgementIDs[messageID])
} }

29
go/cwutil/utils.go Normal file
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
go/gobjects/contact.go Normal file
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
go/gobjects/letter.go Normal file
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
go/gobjects/message.go Normal file
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
go/gothings/gcd.go Normal file
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
go/gothings/uistate.go Normal file
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
go/the/globals.go Normal file
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

394
main.go
View File

@ -1,84 +1,73 @@
package main package main
import ( import (
libpeer "cwtch.im/cwtch/peer" "bounce/go/characters"
"cwtch.im/cwtch/peer/connections" "bounce/go/cwutil"
"encoding/base32" "bounce/go/the"
"fmt" libapp "cwtch.im/cwtch/app"
"github.com/sethvargo/go-diceware/diceware" "cwtch.im/cwtch/model"
"github.com/therecipe/qt/core" "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/quick"
"github.com/therecipe/qt/quickcontrols2" "github.com/therecipe/qt/quickcontrols2"
"github.com/therecipe/qt/widgets" "github.com/therecipe/qt/widgets"
"log"
"os" "os"
"os/user" "os/user"
"path" "path"
"strings" "bounce/go/cwtchthings"
"time" "bounce/go/gothings"
"strconv" "bounce/go/gobjects"
"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"
) )
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 (this *Contact) AddMessage(m Message) {
this.Messages = append(this.Messages, m)
}
type Message struct {
With, Message string
FromMe bool
MessageID uint
Timestamp time.Time
}
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}
}
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)
}
}
func init() { func init() {
GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher") // make go-defined types available in qml
gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
} }
func main() { func main() {
contactMgr = make(ContactManager) // our globals
acknowledgementIDs = make(map[uint32]uint) 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)
}
// window construction boilerplate
view := initializeQtView()
// variables we want to access from inside qml
view.RootContext().SetContextProperty("gcd", gcd)
// this actually loads the qml
view.SetSource(qmlSource)
// 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)
// here we go!
view.Show()
widgets.QApplication_Exec()
}
// window construction boilerplate
func initializeQtView() *quick.QQuickView {
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true) core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true)
widgets.NewQApplication(len(os.Args), os.Args) widgets.NewQApplication(len(os.Args), os.Args)
quickcontrols2.QQuickStyle_SetStyle("Universe") quickcontrols2.QQuickStyle_SetStyle("Universe")
@ -87,176 +76,23 @@ func main() {
view.SetMinimumHeight(280) view.SetMinimumHeight(280)
view.SetMinimumWidth(300) view.SetMinimumWidth(300)
view.SetTitle("cwtch") view.SetTitle("cwtch")
gcd = NewGrandCentralDispatcher(nil)
view.RootContext().SetContextProperty("gcd", gcd)
if len(os.Args) == 2 && os.Args[1] == "local" { return view
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() { // this is mostly going to get factored out when we add profile support
for { // for now, it loads a single peer and fills the ui with its data
time.Sleep(time.Second * 4) func loadCwtchData(gcd *gothings.GrandCentralDispatcher) {
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
}
})
}
}
func initialize(view *quick.QQuickView) {
var err error 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") != "" { if os.Getenv("CWTCH_FOLDER") != "" {
dirname = os.Getenv("CWTCH_FOLDER") the.CwtchDir = os.Getenv("CWTCH_FOLDER")
filename = path.Join(dirname, "keep-this-file-private")
} else { } else {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {
fmt.Printf("\nerror: could not load current user: %v\n", err) fmt.Printf("\nerror: could not load current user: %v\n", err)
os.Exit(1) os.Exit(1)
} }
dirname = path.Join(usr.HomeDir, ".cwtch") the.CwtchDir = path.Join(usr.HomeDir, ".cwtch")
filename = path.Join(dirname, "keep-this-file-private")
} }
/*_, err := app2.NewApp(dirname, "/data/data/org.qtproject.example.go/lib/libtor.so") /*_, 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) 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") the.CwtchApp, err = libapp.NewApp(the.CwtchDir, "tor")
log.Printf("starting Swtch app: err: %v\n", err)
peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime")
if err != nil { 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 create cwtch app: %v", err)
names, err := diceware.Generate(1)
peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename)
if err != nil {
fmt.Println("couldn't create one either :( exiting")
os.Exit(1)
}
peer.Save()
} }
gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion)) err = the.CwtchApp.LoadProfiles("be gay do crime")
if err != nil {
//TODO no more fatalfs
log.Fatalf("couldn't load profiles: %v", err)
}
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 {
log.Fatalf("couldn't create one. is your cwtch folder writable?")
}
the.CwtchApp.SaveProfile(the.Peer)
} else {
the.Peer = the.CwtchApp.PrimaryIdentity()
}
gcd.UpdateMyProfile(the.Peer.GetProfile().Name, the.Peer.GetProfile().Onion, cwutil.RandomProfileImage(the.Peer.GetProfile().Onion))
aif := application.ApplicationInstanceFactory{} aif := application.ApplicationInstanceFactory{}
aif.Init() aif.Init()
app := new(application.RicochetApplication) app := new(application.RicochetApplication)
aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler { aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
ccl := new(ChatChannelListener) ccl := new(cwtchthings.ChatChannelListener)
ccl.Init(rai, app) ccl.Init(rai, app, gcd.UIState.AddMessage, gcd.Acknowledged)
return func() channels.Handler { return func() channels.Handler {
chat := new(channels.ChatChannel) chat := new(channels.ChatChannel)
chat.Handler = ccl chat.Handler = ccl
return chat return chat
} }
}) })
peer.SetApplicationInstanceFactory(aif) the.Peer.SetApplicationInstanceFactory(aif)
the.CwtchApp.LaunchPeers()
contacts := peer.GetContacts()
contacts := the.Peer.GetContacts()
for i := range contacts { for i := range contacts {
attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name") contact, _ := the.Peer.GetProfile().GetContact(contacts[i])
contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0} displayName, _ := contact.GetAttribute("name")
gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted) 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 { 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) group.NewMessage = make(chan model.Message)
go cwtchListener(groups[i], group.NewMessage) go characters.CwtchListener(gcd.UIState.AddMessage, groups[i], group.NewMessage)
peer.JoinServer(group.GroupServer) the.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.UIState.AddContact(&gobjects.Contact{
gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted) group.GroupID,
log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer) group.GroupID[:12],
cwutil.RandomGroupImage(group.GroupID),
group.GroupServer,
0,
0,
group.Accepted,
})
} }
}
// 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"
}
return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
} }

View File

@ -6,6 +6,7 @@ import QtQuick.Layouts 1.3
import QtQuick.Window 2.11 import QtQuick.Window 2.11
import "fonts/Twemoji.js" as T import "fonts/Twemoji.js" as T
import "panes"
import "widgets" import "widgets"
Item { Item {
@ -133,6 +134,7 @@ Item {
readonly property int settingsPane: 2 readonly property int settingsPane: 2
readonly property int userProfilePane: 3 readonly property int userProfilePane: 3
readonly property int groupProfilePane: 4 readonly property int groupProfilePane: 4
readonly property int addGroupPane: 5
Item {} // empty Item {} // empty
@ -141,47 +143,7 @@ Item {
anchors.fill: parent anchors.fill: parent
} }
ColumnLayout { // settingsPane 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
}
}
ColumnLayout { // userProfilePane ColumnLayout { // userProfilePane
anchors.fill: parent anchors.fill: parent
@ -197,21 +159,9 @@ Item {
Label { text: "per-user things like contact name and picture will be edited here" } Label { text: "per-user things like contact name and picture will be edited here" }
} }
Label { // groupProfilePane GroupSettingsPane{}
font.pixelSize: 12
text: "invite new people or change the group name here"
AddGroupPane { anchors.fill: parent }
StackToolbar { text: "Group settings" }
}
Label { // addGroupPane
font.pixelSize: 12
text: "add a new group"
StackToolbar { text: "Create group" }
}
} }
} }
} }

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
}
}

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
}
}
}

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
}
}

View File

@ -8,11 +8,11 @@ ColumnLayout {
id: root id: root
MyProfile{ MyProfile{ // CURRENT PROFILE INFO AND CONTROL BAR
id: myprof id: myprof
} }
Flickable { // CONTACT LIST Flickable { // THE ACTUAL CONTACT LIST
id: sv id: sv
//Layout.alignment: Qt.AlignLeft | Qt.AlignTop //Layout.alignment: Qt.AlignLeft | Qt.AlignTop
clip: true clip: true
@ -36,18 +36,18 @@ ColumnLayout {
width: root.width width: root.width
spacing: 0 spacing: 0
Connections { // ADD/REMOVE CONTACT ENTRIES Connections { // ADD/REMOVE CONTACT ENTRIES
target: gcd target: gcd
onAddContact: function(name, onion, server, image, badge, trusted) { onAddContact: function(handle, displayName, image, server, badge, status, trusted) {
contactsModel.append({ contactsModel.append({
"n": name, "_handle": handle,
"o": onion, "_displayName": displayName,
"s": server, "_image": image,
"i": image, "_server": server,
"b": badge, "_badge": badge,
"t": trusted "_status": status,
"_trusted": trusted,
}) })
} }
} }
@ -56,16 +56,16 @@ ColumnLayout {
id: contactsModel id: contactsModel
} }
Repeater { Repeater {
model: contactsModel // ... AND DISPLAYED HERE model: contactsModel // ... AND DISPLAYED HERE
delegate: Contact{ delegate: ContactRow {
nick: n handle: _handle
onion: o displayName: _displayName
server: s image: _image
image: i server: _server
badge: b badge: _badge
isTrusted: t status: _status
trusted: _trusted
} }
} }
} }

View File

@ -8,15 +8,15 @@ import CustomQmlTypes 1.0
RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
visible: nick != "" visible: true
property alias nick: cn.text property alias displayName: cn.text
property alias image: imgProfile.source property alias image: imgProfile.source
property string onion property string handle
property string badge property int badge
property bool isActive property bool isActive
property bool isHover property bool isHover
property bool isTrusted property bool trusted
property alias status: imgProfile.status property alias status: imgProfile.status
property string server property string server
@ -48,7 +48,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
anchors.left: imgProfile.right anchors.left: imgProfile.right
anchors.right: rectUnread.left anchors.right: rectUnread.left
font.pixelSize: 16 font.pixelSize: 16
font.italic: !isTrusted font.italic: !trusted
textFormat: Text.PlainText textFormat: Text.PlainText
//fontSizeMode: Text.HorizontalFit //fontSizeMode: Text.HorizontalFit
elide: Text.ElideRight elide: Text.ElideRight
@ -61,7 +61,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
width: lblUnread.width + 10 width: lblUnread.width + 10
radius: 8 radius: 8
color: "#4B3557" color: "#4B3557"
visible: badge != "0" visible: badge != 0
anchors.rightMargin: 9 anchors.rightMargin: 9
@ -85,7 +85,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
gcd.broadcast("ResetMessagePane") gcd.broadcast("ResetMessagePane")
isActive = true isActive = true
theStack.pane = theStack.messagePane theStack.pane = theStack.messagePane
gcd.loadMessagesPane(onion) gcd.loadMessagesPane(handle)
} }
onEntered: { onEntered: {
@ -100,25 +100,18 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
Connections { // UPDATE UNREAD MESSAGES COUNTER Connections { // UPDATE UNREAD MESSAGES COUNTER
target: gcd target: gcd
onSetUnread: function(foronion, n) {
if (onion == foronion) {
badge = ""+n
}
}
onResetMessagePane: function() { onResetMessagePane: function() {
isActive = false isActive = false
} }
onSetConnectionStatus: function(foronion, x) { onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted) {
if (onion == foronion) { if (handle == _handle) {
status = x displayName = _displayName
} image = _image
} server = _server
badge = _badge
onMarkTrusted: function(foronion) { status = _status
if (onion == foronion) { trusted = _trusted
isTrusted = true
} }
} }
} }

View File

@ -15,10 +15,12 @@ RowLayout {
property alias message: lbl.text property alias message: lbl.text
property string from property string from
property string displayname property string handle
property string displayName
property int messageID property int messageID
property bool fromMe
property alias timestamp: ts.text property alias timestamp: ts.text
property alias source: imgProfile.source property alias image: imgProfile.source
property alias status: imgProfile.status property alias status: imgProfile.status
@ -107,14 +109,14 @@ RowLayout {
color: "#FFFFFF" color: "#FFFFFF"
font.pixelSize: 10 font.pixelSize: 10
anchors.right: parent.right anchors.right: parent.right
text: displayname text: displayName
visible: from != "me" visible: from != "me"
} }
Image { // ACKNOWLEDGEMENT ICON Image { // ACKNOWLEDGEMENT ICON
id: ack id: ack
anchors.right: parent.right 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 height: 10
sourceSize.height: 10 sourceSize.height: 10
visible: from == "me" visible: from == "me"

View File

@ -14,7 +14,10 @@ ColumnLayout {
StackToolbar { StackToolbar {
text: "open privacy exec" 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 Flickable { // THE MESSAGE LIST ITSELF
@ -38,14 +41,16 @@ ColumnLayout {
messagesModel.clear() messagesModel.clear()
} }
onAppendMessage: function(from, message, displayname, mid, ts, source) { onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts) {
messagesModel.append({ messagesModel.append({
"f": from, "_handle": handle,
"m": parse(message, 12), "_from": from,
"d": displayname, "_displayName": displayName,
"i": mid, "_message": parse(message, 12),
"t": ts, "_image": image,
"src": source "_mid": mid,
"_fromMe": fromMe,
"_ts": ts,
}) })
if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) { if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) {
@ -73,12 +78,14 @@ ColumnLayout {
Repeater { // ... AND DISPLAYED HERE Repeater { // ... AND DISPLAYED HERE
model: messagesModel model: messagesModel
delegate: Message { delegate: Message {
from: f handle: _handle
message: m from: _from
displayname: d displayName: _displayName
messageID: i message: _message
timestamp: t image: _image
source: src messageID: _mid
fromMe: _fromMe
timestamp: _ts
} }
} }
} }
@ -88,8 +95,8 @@ ColumnLayout {
Rectangle { // MESSAGE ENTRY TEXTFIELD Rectangle { // MESSAGE ENTRY TEXTFIELD
id: rectMessage id: rectMessage
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 40 * zoomSlider.value Layout.minimumHeight: 40 * gcd.themeScale
Layout.maximumHeight: 40 * zoomSlider.value Layout.maximumHeight: 40 * gcd.themeScale
color: "#EDEDED" color: "#EDEDED"
border.color: "#AAAAAA" border.color: "#AAAAAA"
radius: 10 radius: 10

View File

@ -149,7 +149,7 @@ ColumnLayout {
Row { // TOOLS FOR EDITING PROFILE Row { // TOOLS FOR EDITING PROFILE
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: zoomSlider.value * 2 spacing: gcd.themeScale * 2
TextEdit { // USED TO POWER THE COPY/PASTE BUTTON TextEdit { // USED TO POWER THE COPY/PASTE BUTTON
@ -164,8 +164,8 @@ ColumnLayout {
onClicked: { onClicked: {
gcd.popup("copied to clipboard!") gcd.popup("copied to clipboard!")
txtHidden.text = nick.replace(" ", "~") + "~" + onion txtHidden.text = nick.replace(" ", "~") + "~" + onion
txtHidden.selectAll(); txtHidden.selectAll()
txtHidden.copy(); 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 { RowLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right

Binary file not shown.

View File

@ -7,7 +7,7 @@ import QtQuick.Window 2.11
Label { Label {
font.pixelSize: zoomSlider.value * size font.pixelSize: gcd.themeScale * size
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
property real size: 12 property real size: 12

View File

@ -9,10 +9,10 @@ import "../fonts/Twemoji.js" as T
Rectangle { Rectangle {
id: button 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.minimumWidth: width
Layout.maximumWidth: width Layout.maximumWidth: width
height: 20 * zoomSlider.value height: 20 * gcd.themeScale
Layout.minimumHeight: height Layout.minimumHeight: height
Layout.maximumHeight: height Layout.maximumHeight: height
color: mousedown ? "#B09CBC" : "#4B3557" color: mousedown ? "#B09CBC" : "#4B3557"

Binary file not shown.

View File

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