dan
/
ui
forked from cwtch.im/ui
1
0
Fork 0

initial commit

master
erinn 4 years ago
parent 690a71308e
commit 8a9ba6d154
  1. 237
      gcd.go
  2. 39
      go/characters/cwtchlistener.go
  3. 21
      go/characters/grouppoller.go
  4. 39
      go/characters/postmanpat.go
  5. 49
      go/characters/presencepoller.go
  6. 35
      go/characters/torstatuspoller.go
  7. 3
      go/constants/style.go
  8. 33
      go/cwtchthings/chatchannellistener.go
  9. 29
      go/cwutil/utils.go
  10. 11
      go/gobjects/contact.go
  11. 8
      go/gobjects/letter.go
  12. 14
      go/gobjects/message.go
  13. 303
      go/gothings/gcd.go
  14. 116
      go/gothings/uistate.go
  15. 11
      go/the/globals.go
  16. 380
      main.go
  17. 60
      qml/main.qml
  18. 53
      qml/panes/AddGroupPane.qml
  19. 68
      qml/panes/GroupSettingsPane.qml
  20. 50
      qml/panes/SettingsPane.qml
  21. 36
      qml/widgets/ContactList.qml
  22. 39
      qml/widgets/ContactRow.qml
  23. 10
      qml/widgets/Message.qml
  24. 39
      qml/widgets/MessageList.qml
  25. 19
      qml/widgets/MyProfile.qml
  26. BIN
      qml/widgets/MyProfile.qmlc
  27. 2
      qml/widgets/ScalingLabel.qml
  28. 4
      qml/widgets/SimpleButton.qml
  29. BIN
      qml/widgets/SimpleButton.qmlc
  30. 2
      qml/widgets/StackToolbar.qml

237
gcd.go

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

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

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

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

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

@ -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"])
}
}

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

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

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

@ -0,0 +1,11 @@
package gobjects
type Contact struct {
Handle string
DisplayName string
Image string
Server string
Badge int
Status int
Trusted bool
}

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

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

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

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

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

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