diff --git a/.gitignore b/.gitignore index 485dee6..138bbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea +*.session diff --git a/config/bridge.go b/config/bridge.go new file mode 100644 index 0000000..97ebaf8 --- /dev/null +++ b/config/bridge.go @@ -0,0 +1,76 @@ +// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "bytes" + "text/template" +) + +type BridgeConfig struct { + RawUsernameTemplate string `yaml:"username_template"` + RawDisplaynameTemplate string `yaml:"displayname_template"` + UsernameTemplate *template.Template `yaml:"-"` + DisplaynameTemplate *template.Template `yaml:"-"` +} + +func (bc BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + err := unmarshal(bc) + if err != nil { + return err + } + + bc.UsernameTemplate, err = template.New("username").Parse(bc.RawUsernameTemplate) + if err != nil { + return err + } + + bc.DisplaynameTemplate, err = template.New("displayname").Parse(bc.RawDisplaynameTemplate) + return err +} + +type DisplaynameTemplateArgs struct { + Displayname string +} + +type UsernameTemplateArgs struct { + Receiver string + UserID string +} + +func (bc BridgeConfig) FormatDisplayname(displayname string) string { + var buf bytes.Buffer + bc.DisplaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{ + Displayname: displayname, + }) + return buf.String() +} + +func (bc BridgeConfig) FormatUsername(receiver, userID string) string { + var buf bytes.Buffer + bc.UsernameTemplate.Execute(&buf, UsernameTemplateArgs{ + Receiver: receiver, + UserID: userID, + }) + return buf.String() +} + +func (bc BridgeConfig) MarshalYAML() (interface{}, error) { + bc.RawDisplaynameTemplate = bc.FormatDisplayname("{{.Displayname}}") + bc.RawUsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}") + return bc, nil +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9690258 --- /dev/null +++ b/config/config.go @@ -0,0 +1,81 @@ +// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "maunium.net/go/mautrix-appservice" + "io/ioutil" + "gopkg.in/yaml.v2" +) + +type Config struct { + Homeserver struct { + Address string `yaml:"address"` + Domain string `yaml:"domain"` + } `yaml:"homeserver"` + + AppService struct { + Address string `yaml:"address"` + Hostname string `yaml:"hostname"` + Port uint16 `yaml:"port"` + + Database string `yaml:"database"` + + ID string `yaml:"id"` + Bot struct { + Username string `yaml:"username"` + Displayname string `yaml:"displayname"` + Avatar string `yaml:"avatar"` + } `yaml:"bot"` + + ASToken string `yaml:"as_token"` + HSToken string `yaml:"hs_token"` + } `yaml:"appservice"` + + Bridge BridgeConfig `yaml:"bridge"` + + Logging appservice.LogConfig `yaml:"logging"` +} + +func Load(path string) (*Config, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var config = &Config{} + err = yaml.Unmarshal(data, config) + return config, err +} + +func (config *Config) Save(path string) error { + data, err := yaml.Marshal(config) + if err != nil { + return err + } + return ioutil.WriteFile(path, data, 0600) +} + +func (config *Config) Appservice() (*appservice.Config, error) { + as := appservice.Create() + as.LogConfig = config.Logging + as.HomeserverDomain = config.Homeserver.Domain + as.HomeserverURL = config.Homeserver.Address + var err error + as.Registration, err = config.GetRegistration() + return as, err +} diff --git a/config/registration.go b/config/registration.go new file mode 100644 index 0000000..429e3b3 --- /dev/null +++ b/config/registration.go @@ -0,0 +1,62 @@ +// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "maunium.net/go/mautrix-appservice" + "regexp" +) + +func (config *Config) NewRegistration() (*appservice.Registration, error) { + registration := appservice.CreateRegistration("mautrix-whatsapp") + + err := config.copyToRegistration(registration) + if err != nil { + return nil, err + } + + config.AppService.ASToken = registration.AppToken + config.AppService.HSToken = registration.ServerToken + return registration, nil +} + +func (config *Config) GetRegistration() (*appservice.Registration, error) { + registration := appservice.CreateRegistration("mautrix-whatsapp") + + err := config.copyToRegistration(registration) + if err != nil { + return nil, err + } + + registration.AppToken = config.AppService.ASToken + registration.ServerToken = config.AppService.HSToken + return registration, nil +} + +func (config *Config) copyToRegistration(registration *appservice.Registration) error { + registration.ID = config.AppService.ID + registration.URL = config.AppService.Address + registration.RateLimited = false + registration.SenderLocalpart = config.AppService.Bot.Username + + userIDRegex, err := regexp.Compile(config.Bridge.FormatUsername("[0-9]+", "[0-9]+")) + if err != nil { + return err + } + registration.Namespaces.RegisterUserIDs(userIDRegex, true) + return nil +} diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..d43b15c --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,60 @@ +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + address: https://matrix.org + # The domain of the homeserver (for MXIDs, etc). + domain: matrix.org + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration. +appservice: + # The address that the homeserver can use to connect to this appservice. + address: http://localhost:8080 + + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 + port: 8080 + + # The full URI to the database. Only SQLite is currently supported. + database: sqlite:///mautrix-whatsapp.db + + # The unique ID of this appservice. + id: whatsapp + # Appservice bot details. + bot: + # Username of the appservice bot. + username: whatsappbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: WhatsApp bridge bot + avatar: mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + +# Bridge config. Currently unused. +bridge: + # Localpart template of MXIDs for Whatsapp users. + # {{.receiver}} is replaced with the Whatsapp user ID of the Matrix user receiving messages. + # {{.userid}} is replaced with the user ID of the Whatsapp user. + username_template: "whatsapp_{{.Receiver}}_{{.UserID}}" + # Displayname template for Whatsapp users. + # {{.displayname}} is replaced with the display name of the Whatsapp user. + displayname_template: "{{.Displayname}}" + +# Logging config. +logging: + # The directory for log files. Will be created if not found. + directory: ./logs + # Available variables: .date for the file date and .index for different log files on the same day. + file_name_format: {{.date}}-{{.index}.log + # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants + file_date_format: 2006-01-02 + # Log file permissions. + file_mode: 0600 + # Timestamp format for log entries in the Go time format. + timestamp_format: Jan _2, 2006 15:04:05 + # Minimum severity for log messages. + # Options: debug, info, warn, error, fatal + print_level: info diff --git a/main.go b/main.go index 2565072..70523be 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// mautrix-whatsapp - A puppeting Matrix-Whatsapp bridge. +// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge. // Copyright (C) 2018 Tulir Asokan // // This program is free software: you can redistribute it and/or modify @@ -16,6 +16,99 @@ package main +import ( + "github.com/Rhymen/go-whatsapp" + "time" + "fmt" + "os" + "bufio" + "encoding/gob" + "github.com/mdp/qrterminal" +) + func main() { - + wac, err := whatsapp.NewConn(20 * time.Second) + if err != nil { + panic(err) + } + + wac.AddHandler(myHandler{}) + + sess, err := LoadSession("whatsapp.session") + if err != nil { + fmt.Println(err) + sess, err = Login(wac) + } else { + sess, err = wac.RestoreSession(sess) + } + if err != nil { + panic(err) + } + SaveSession(sess, "whatsapp.session") + + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("receiver> ") + receiver, _ := reader.ReadString('\n') + fmt.Print("message> ") + message, _ := reader.ReadString('\n') + wac.Send(whatsapp.TextMessage{ + Info: whatsapp.MessageInfo{ + RemoteJid: fmt.Sprintf("%s@s.whatsapp.net", receiver), + }, + Text: message, + }) + fmt.Println(receiver, message) + } +} + +func Login(wac *whatsapp.Conn) (whatsapp.Session, error) { + qrChan := make(chan string) + go func() { + qrterminal.Generate(<-qrChan, qrterminal.L, os.Stdout) + }() + return wac.Login(qrChan) +} + +func SaveSession(session whatsapp.Session, fileName string) { + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + panic(err) + } + + enc := gob.NewEncoder(file) + enc.Encode(session) +} + +func LoadSession(fileName string) (sess whatsapp.Session, err error) { + file, err := os.OpenFile(fileName, os.O_RDONLY, 0600) + if err != nil { + return sess, err + } + + dec := gob.NewDecoder(file) + dec.Decode(sess) + return +} + +type myHandler struct{} + +func (myHandler) HandleError(err error) { + fmt.Fprintf(os.Stderr, "%v", err) +} + +func (myHandler) HandleTextMessage(message whatsapp.TextMessage) { + fmt.Println(message) +} + +func (myHandler) HandleImageMessage(message whatsapp.ImageMessage) { + fmt.Println(message) +} + +func (myHandler) HandleVideoMessage(message whatsapp.VideoMessage) { + fmt.Println(message) +} + +func (myHandler) HandleJsonMessage(message string) { + fmt.Println(message) } diff --git a/matrix.go b/matrix.go new file mode 100644 index 0000000..b78268b --- /dev/null +++ b/matrix.go @@ -0,0 +1,21 @@ +// mautrix-whatsapp - A Matrix-Whatsapp puppeting bridge. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +func InitMatrix() { + +}