Compare commits
28 Commits
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | 055e1d65a1 | |
Sarah Jamie Lewis | 8a7c647ea0 | |
Sarah Jamie Lewis | 5871a2c077 | |
Sarah Jamie Lewis | ae179fab72 | |
Sarah Jamie Lewis | dd411bd337 | |
Sarah Jamie Lewis | 1d3d2878b7 | |
Sarah Jamie Lewis | 28fe3b21fb | |
Sarah Jamie Lewis | 20897a9f8d | |
Sarah Jamie Lewis | 709d377bf4 | |
Sarah Jamie Lewis | 1baac147d7 | |
Sarah Jamie Lewis | f152b02230 | |
Sarah Jamie Lewis | bef3f11150 | |
Sarah Jamie Lewis | b29293334d | |
Sarah Jamie Lewis | 9da33c3083 | |
Sarah Jamie Lewis | 3d0a3a5a49 | |
Sarah Jamie Lewis | 1e4221c6bd | |
Dan Ballard | 973e73a308 | |
Sarah Jamie Lewis | ff20656e22 | |
Sarah Jamie Lewis | e12cb2c965 | |
Sarah Jamie Lewis | 54ba8c463a | |
Sarah Jamie Lewis | 137027d011 | |
Dan Ballard | 916ca279ea | |
Sarah Jamie Lewis | 0d05a0731c | |
Dan Ballard | e7e9a71515 | |
Dan Ballard | 6021daeaca | |
Sarah Jamie Lewis | 92ec4c6667 | |
Sarah Jamie Lewis | d28d0db77c | |
Dan Ballard | 18c6907dbe |
|
@ -14,7 +14,6 @@ steps:
|
|||
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
|
||||
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
|
||||
- chmod a+x tor
|
||||
- go get -u golang.org/x/lint/golint
|
||||
- git fetch --tags
|
||||
#- export GO111MODULE=on
|
||||
#- go mod vendor
|
||||
|
@ -43,7 +42,7 @@ steps:
|
|||
- make linux
|
||||
|
||||
- name: build-android
|
||||
image: openpriv/android-go-mobile:2022.11
|
||||
image: openpriv/android-go-mobile:2023.02
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
|
@ -53,7 +52,7 @@ steps:
|
|||
- make android
|
||||
|
||||
- name: build-windows
|
||||
image: openpriv/mingw-go:2022.11
|
||||
image: openpriv/mingw-go:2023.01
|
||||
environment:
|
||||
GOPATH: /go
|
||||
volumes:
|
||||
|
|
14
Makefile
14
Makefile
|
@ -16,23 +16,27 @@ windows: libCwtch.dll
|
|||
|
||||
libCwtch.so: lib.go
|
||||
./switch-ffi.sh
|
||||
go build -ldflags "-X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell date +%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.so
|
||||
go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.so
|
||||
|
||||
libCwtch.x64.dylib: lib.go
|
||||
./switch-ffi.sh
|
||||
go build -ldflags "-X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell date +%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.x64.dylib
|
||||
go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.x64.dylib
|
||||
|
||||
libCwtch.arm64.dylib: lib.go
|
||||
./switch-ffi.sh
|
||||
env GOARCH=arm64 GOOS=darwin CGO_ENABLED=1 go build -ldflags "-X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell date +%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.arm64.dylib
|
||||
env GOARCH=arm64 GOOS=darwin CGO_ENABLED=1 go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.arm64.dylib
|
||||
|
||||
cwtch.aar: lib.go
|
||||
./switch-gomobile.sh
|
||||
gomobile bind -target android/arm,android/arm64,android/amd64 -ldflags="-X cwtch.buildVer=$(shell git describe --tags) -X cwtch.buildDate=$(shell date +%G-%m-%d-%H-%M)"
|
||||
gomobile bind -trimpath -target android/arm,android/arm64,android/amd64 -ldflags="-buildid=$(shell git describe --tags) -X cwtch.buildVer=$(shell git describe --tags) -X cwtch.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)"
|
||||
|
||||
libCwtch.dll: lib.go
|
||||
./switch-ffi.sh
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -ldflags "-X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell date +%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.dll
|
||||
# '-Xlinker --no-insert-timestamp` sets the output dll PE timestamp header to all zeros, instead of the actual time
|
||||
# this is necessary for reproducible builds (see: https://wiki.debian.org/ReproducibleBuilds/TimestampsInPEBinaries for additional information)
|
||||
# note: the above documentation also references an ability to set an optional timestamp - this behaviour seems to no longer be supported in more recent versions of mingw32-gcc (the help docs no longer reference that functionality)
|
||||
# these flags have to be passed through to the underlying gcc process using the -extldflags option in the underlying go linker, note that the whole flag is quoted...this is necessary.
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M) '-extldflags=-Xlinker --no-insert-timestamp'" -buildmode c-shared -o libCwtch.dll
|
||||
|
||||
clean:
|
||||
rm -f cwtch.aar cwtch_go.apk libCwtch.h libCwtch.so cwtch-sources.jar libCwtch.dll libCwtch.dylib
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# NOTE: libcwtch-go has been deprecated in favour of [autobindings](https://git.openprivacy.ca/cwtch.im/autobindings). This repository has been archived and is no longer maintained.
|
||||
|
||||
# libcwtch-go
|
||||
|
||||
C-bindings for the Go Cwtch Library.
|
||||
|
|
|
@ -16,6 +16,8 @@ const ProfileTypeV1Password = "v1-userPassword"
|
|||
// PeerOnline stores state on if the peer believes it is online
|
||||
const PeerOnline = "peer-online"
|
||||
|
||||
const PeerAutostart = "autostart"
|
||||
|
||||
// Description is used on server contacts,
|
||||
const Description = "description"
|
||||
|
||||
|
|
4
go.mod
4
go.mod
|
@ -3,7 +3,7 @@ module git.openprivacy.ca/cwtch.im/libcwtch-go
|
|||
go 1.17
|
||||
|
||||
require (
|
||||
cwtch.im/cwtch v0.18.4
|
||||
cwtch.im/cwtch v0.18.10
|
||||
git.openprivacy.ca/cwtch.im/server v1.4.5
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.8.6
|
||||
git.openprivacy.ca/openprivacy/log v1.0.3
|
||||
|
@ -28,4 +28,4 @@ require (
|
|||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
)
|
||||
)
|
10
go.sum
10
go.sum
|
@ -3,6 +3,16 @@ cwtch.im/cwtch v0.18.3 h1:3zBvC4buII6pWQ+OOVUR6WuAwQDKCxSrj0ZOYKEeB6I=
|
|||
cwtch.im/cwtch v0.18.3/go.mod h1:StheazFFY7PKqBbEyDVLhzWW6WOat41zV0ckC240c5Y=
|
||||
cwtch.im/cwtch v0.18.4 h1:Oht7rEDVJjVWDOKg0xqDgXvY/H059HMJlOPt/nBGqxk=
|
||||
cwtch.im/cwtch v0.18.4/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
cwtch.im/cwtch v0.18.5 h1:yqDns4flbowsbaWjMiUm7Em4IAlM8kkgm79CCcXV1GE=
|
||||
cwtch.im/cwtch v0.18.5/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
cwtch.im/cwtch v0.18.6 h1:CRwoZ/H7y1rAp6jrYh6YCIILU+Sw59hJUvHaWqPgBjg=
|
||||
cwtch.im/cwtch v0.18.6/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
cwtch.im/cwtch v0.18.7 h1:ysE1kjy4oTF+VaQrkNdwdEs6rklWGOe9Dp8rlu9VDKI=
|
||||
cwtch.im/cwtch v0.18.7/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
cwtch.im/cwtch v0.18.8 h1:D5mmsBkmHhE7jhRodZG2DtdaxmfvdvLG0W7CAPBf7eo=
|
||||
cwtch.im/cwtch v0.18.8/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
cwtch.im/cwtch v0.18.10 h1:iTzLzlms1mgn8kLfClU/yAWIVWVRRT8UmfbDNli9dzE=
|
||||
cwtch.im/cwtch v0.18.10/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
|
||||
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
|
|
52
lib.go
52
lib.go
|
@ -238,6 +238,7 @@ func buildACN(settings utils.GlobalSettings, torPath string, appDir string) conn
|
|||
}
|
||||
|
||||
torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key))
|
||||
// torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice)
|
||||
if settings.UseCustomTorrc {
|
||||
customTorrc := settings.CustomTorrc
|
||||
torrc.WithCustom(strings.Split(customTorrc, "\n"))
|
||||
|
@ -519,17 +520,47 @@ func GetAppBusEvent() string {
|
|||
return json
|
||||
}
|
||||
|
||||
//export c_CreateProfile
|
||||
func c_CreateProfile(nick_ptr *C.char, nick_len C.int, pass_ptr *C.char, pass_len C.int) {
|
||||
CreateProfile(C.GoStringN(nick_ptr, nick_len), C.GoStringN(pass_ptr, pass_len))
|
||||
//export c_ActivatePeerEngine
|
||||
func c_ActivatePeerEngine(onion_ptr *C.char, onion_len C.int) {
|
||||
ActivatePeerEngine(C.GoStringN(onion_ptr, onion_len))
|
||||
}
|
||||
|
||||
func CreateProfile(nick, pass string) {
|
||||
if pass == constants.DefactoPasswordForUnencryptedProfiles {
|
||||
application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1DefaultPassword)
|
||||
} else {
|
||||
application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1Password)
|
||||
func ActivatePeerEngine(profile string) {
|
||||
doServers := false
|
||||
if _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments); err == nil {
|
||||
doServers = true
|
||||
}
|
||||
application.ActivatePeerEngine(profile, true, true, doServers)
|
||||
}
|
||||
|
||||
//export c_DeactivatePeerEngine
|
||||
func c_DeactivatePeerEngine(onion_ptr *C.char, onion_len C.int) {
|
||||
DeactivatePeerEngine(C.GoStringN(onion_ptr, onion_len))
|
||||
}
|
||||
|
||||
func DeactivatePeerEngine(profile string) {
|
||||
application.DeactivatePeerEngine(profile)
|
||||
}
|
||||
|
||||
//export c_CreateProfile
|
||||
func c_CreateProfile(nick_ptr *C.char, nick_len C.int, pass_ptr *C.char, pass_len C.int, autostart C.char) {
|
||||
CreateProfile(C.GoStringN(nick_ptr, nick_len), C.GoStringN(pass_ptr, pass_len), autostart == 1)
|
||||
}
|
||||
|
||||
func CreateProfile(nick, pass string, autostart bool) {
|
||||
autostartVal := constants2.True
|
||||
if !autostart {
|
||||
autostartVal = constants2.False
|
||||
}
|
||||
tagVal := constants.ProfileTypeV1Password
|
||||
if pass == constants.DefactoPasswordForUnencryptedProfiles {
|
||||
tagVal = constants.ProfileTypeV1DefaultPassword
|
||||
}
|
||||
|
||||
application.CreatePeer(nick, pass, map[attr.ZonedPath]string{
|
||||
attr.ProfileZone.ConstructZonedPath(constants2.Tag): tagVal,
|
||||
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
|
||||
})
|
||||
}
|
||||
|
||||
//export c_LoadProfiles
|
||||
|
@ -914,7 +945,8 @@ func DownloadFile(profileOnion string, conversationID int, filepath, manifestpat
|
|||
log.Errorf("file sharing error: %v", err)
|
||||
} else {
|
||||
// default to max 10 GB limit...
|
||||
fh.DownloadFile(profile, conversationID, filepath, manifestpath, filekey, files.MaxManifestSize*files.DefaultChunkSize)
|
||||
err := fh.DownloadFile(profile, conversationID, filepath, manifestpath, filekey, files.MaxManifestSize*files.DefaultChunkSize)
|
||||
log.Errorf("file sharing error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1110,6 +1142,8 @@ func SetProfileAttribute(profileOnion string, key string, value string) {
|
|||
// All other scopes and zones need to be added explicitly or handled by Cwtch.
|
||||
if zone == attr.ProfileZone && key == constants.Name {
|
||||
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, value)
|
||||
} else if zone == attr.ProfileZone && key == constants.PeerAutostart {
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, value)
|
||||
} else {
|
||||
log.Errorf("attempted to set an attribute with an unknown zone: %v", key)
|
||||
}
|
||||
|
|
|
@ -100,14 +100,17 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
if newAcnStatus == 100 {
|
||||
if acnStatus != 100 {
|
||||
// just came online
|
||||
doServers := false
|
||||
if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil {
|
||||
doServers = true
|
||||
}
|
||||
|
||||
for _, onion := range eh.app.ListProfiles() {
|
||||
// launch a listen thread (internally this does a check that the protocol engine is not listening)
|
||||
// and as such is safe to call.
|
||||
doServers := false
|
||||
if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil {
|
||||
doServers = true
|
||||
profile := eh.app.GetPeer(onion)
|
||||
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart)
|
||||
if !exists || autostart == "true" {
|
||||
eh.app.ActivatePeerEngine(onion, true, true, doServers)
|
||||
}
|
||||
eh.app.ActivePeerEngine(onion, true, true, doServers)
|
||||
}
|
||||
eh.api.LaunchServers()
|
||||
}
|
||||
|
@ -121,7 +124,6 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
}
|
||||
}
|
||||
acnStatus = newAcnStatus
|
||||
|
||||
case event.NewPeer:
|
||||
onion := e.Data[event.Identity]
|
||||
profile := eh.app.GetPeer(e.Data[event.Identity])
|
||||
|
@ -144,8 +146,8 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
}
|
||||
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline, event.False)
|
||||
eh.app.AddPeerPlugin(onion, plugins.CONNECTIONRETRY)
|
||||
eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK)
|
||||
// disabeling network check for connection attempt reservation, needs rework
|
||||
//eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK)
|
||||
eh.app.AddPeerPlugin(onion, plugins.ANTISPAM)
|
||||
|
||||
// If the user has chosen to block unknown profiles
|
||||
|
@ -160,14 +162,26 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
|
||||
// Start up the Profile
|
||||
if acnStatus == 100 {
|
||||
doServers := false
|
||||
if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil {
|
||||
doServers = true
|
||||
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart)
|
||||
if !exists || autostart == "true" {
|
||||
doServers := false
|
||||
if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil {
|
||||
doServers = true
|
||||
}
|
||||
eh.app.ActivatePeerEngine(onion, true, true, doServers)
|
||||
}
|
||||
eh.app.ActivePeerEngine(onion, true, true, doServers)
|
||||
}
|
||||
|
||||
online, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline)
|
||||
e.Data["Online"] = online
|
||||
|
||||
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart)
|
||||
// legacy profiles should autostart by default
|
||||
if !exists {
|
||||
autostart = "true"
|
||||
}
|
||||
e.Data["autostart"] = autostart
|
||||
|
||||
// Name always exists
|
||||
e.Data[constants.Name], _ = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
e.Data[constants2.DefaultProfilePicture] = RandomProfileImage(onion)
|
||||
|
@ -177,21 +191,7 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
e.Data[constants2.Picture] = RandomProfileImage(onion)
|
||||
} else {
|
||||
e.Data[constants2.Picture], _ = profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key))
|
||||
serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key))
|
||||
profile.ShareFile(key, serializedManifest)
|
||||
log.Debugf("Custom Profile Image: %v %s", e.Data[constants2.Picture], serializedManifest)
|
||||
}
|
||||
|
||||
// Resolve the profile image of the profile.
|
||||
|
||||
e.Data["Online"] = online
|
||||
|
||||
// If file sharing is enabled then reshare all active files...
|
||||
fsf, err := filesharing.FunctionalityGate(settings.Experiments)
|
||||
if err == nil {
|
||||
fsf.ReShareFiles(profile)
|
||||
}
|
||||
|
||||
// Construct our conversations and our srever lists
|
||||
var contacts []Contact
|
||||
var servers []groups.Server
|
||||
|
@ -608,6 +608,31 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string {
|
|||
associatedGroupsJson, _ := json.Marshal(associatedGroups)
|
||||
ev.Event.Data[event.Data] = string(associatedGroupsJson)
|
||||
}
|
||||
case event.ProtocolEngineCreated:
|
||||
// TODO this code should be moved into Cwtch during the API officialization...
|
||||
settings := ReadGlobalSettings()
|
||||
|
||||
// ensure that protocol engine respects blocking settings...
|
||||
if settings.BlockUnknownConnections {
|
||||
profile.BlockUnknownConnections()
|
||||
} else {
|
||||
profile.AllowUnknownConnections()
|
||||
}
|
||||
|
||||
// Now that the Peer Engine is Activated, Share Files
|
||||
key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
|
||||
if exists {
|
||||
serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key))
|
||||
// reset the share timestamp, currently file shares are hardcoded to expire after 30 days...
|
||||
// we reset the profile image here so that it is always available.
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10))
|
||||
log.Debugf("Custom Profile Image: %v %s", key, serializedManifest)
|
||||
}
|
||||
// If file sharing is enabled then reshare all active files...
|
||||
fsf, err := filesharing.FunctionalityGate(settings.Experiments)
|
||||
if err == nil {
|
||||
fsf.ReShareFiles(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -649,7 +674,7 @@ func (eh *EventHandler) startHandlingPeer(onion string) {
|
|||
eventBus.Subscribe(event.FileDownloadProgressUpdate, q)
|
||||
eventBus.Subscribe(event.FileDownloaded, q)
|
||||
eventBus.Subscribe(event.TokenManagerInfo, q)
|
||||
|
||||
eventBus.Subscribe(event.ProtocolEngineCreated, q)
|
||||
go eh.forwardProfileMessages(onion, q)
|
||||
|
||||
}
|
||||
|
|
Reference in New Issue