forked from MirrorHub/mautrix-whatsapp
Add portal rooms to user-specific community for filtering
This commit is contained in:
parent
07b8936985
commit
7bf470d69e
7 changed files with 198 additions and 18 deletions
100
community.go
Normal file
100
community.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
appservice "maunium.net/go/mautrix-appservice"
|
||||
)
|
||||
|
||||
func (user *User) inviteToCommunity() {
|
||||
url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", user.MXID)
|
||||
reqBody := map[string]interface{}{}
|
||||
_, err := user.bridge.Bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
|
||||
if err != nil {
|
||||
user.log.Warnln("Failed to invite user to personal filtering community %s: %v", user.CommunityID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (user *User) updateCommunityProfile() {
|
||||
url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "profile")
|
||||
profileReq := struct {
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
ShortDescription string `json:"short_description"`
|
||||
}{"WhatsApp", user.bridge.Config.AppService.Bot.Avatar, "Your WhatsApp bridged chats"}
|
||||
_, err := user.bridge.Bot.MakeRequest(http.MethodPost, url, &profileReq, nil)
|
||||
if err != nil {
|
||||
user.log.Warnln("Failed to update metadata of %s: %v", user.CommunityID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (user *User) createCommunity() {
|
||||
if !user.bridge.Config.Bridge.EnableCommunities() {
|
||||
return
|
||||
}
|
||||
|
||||
localpart, server := appservice.ParseUserID(user.MXID)
|
||||
community := user.bridge.Config.Bridge.FormatCommunity(localpart, server)
|
||||
user.log.Debugln("Creating personal filtering community", community)
|
||||
bot := user.bridge.Bot
|
||||
req := struct {
|
||||
Localpart string `json:"localpart"`
|
||||
}{community}
|
||||
resp := struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}{}
|
||||
_, err := bot.MakeRequest(http.MethodPost, bot.BuildURL("create_group"), &req, &resp)
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(mautrix.HTTPError); ok {
|
||||
if httpErr.RespError.Err != "Group already exists" {
|
||||
user.log.Warnln("Server responded with error creating personal filtering community:", err)
|
||||
return
|
||||
} else {
|
||||
resp.GroupID = fmt.Sprintf("+%s:%s", req.Localpart, user.bridge.Config.Homeserver.Domain)
|
||||
user.log.Debugln("Personal filtering community", resp.GroupID, "already existed")
|
||||
}
|
||||
} else {
|
||||
user.log.Warnln("Unknown error creating personal filtering community:", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user.log.Infoln("Created personal filtering community %s", resp.GroupID)
|
||||
user.inviteToCommunity()
|
||||
user.updateCommunityProfile()
|
||||
}
|
||||
user.CommunityID = resp.GroupID
|
||||
}
|
||||
|
||||
func (user *User) addPortalToCommunity(portal *Portal) bool {
|
||||
if len(user.CommunityID) == 0 {
|
||||
return false
|
||||
}
|
||||
bot := user.bridge.Bot
|
||||
url := bot.BuildURL("groups", user.CommunityID, "admin", "rooms", portal.MXID)
|
||||
reqBody := map[string]interface{}{}
|
||||
_, err := bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
|
||||
if err != nil {
|
||||
user.log.Warnln("Failed to add %s to %s: %v", portal.MXID, user.CommunityID, err)
|
||||
return false
|
||||
}
|
||||
user.log.Debugln("Added", portal.MXID, "to", user.CommunityID)
|
||||
return true
|
||||
}
|
|
@ -32,6 +32,7 @@ import (
|
|||
type BridgeConfig struct {
|
||||
UsernameTemplate string `yaml:"username_template"`
|
||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||
CommunityTemplate string `yaml:"community_template"`
|
||||
|
||||
ConnectionTimeout int `yaml:"connection_timeout"`
|
||||
LoginQRRegenCount int `yaml:"login_qr_regen_count"`
|
||||
|
@ -51,7 +52,7 @@ type BridgeConfig struct {
|
|||
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
|
||||
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
|
||||
|
||||
AllowUserInvite bool `yaml:"allow_user_invite"`
|
||||
AllowUserInvite bool `yaml:"allow_user_invite"`
|
||||
|
||||
CommandPrefix string `yaml:"command_prefix"`
|
||||
|
||||
|
@ -59,6 +60,7 @@ type BridgeConfig struct {
|
|||
|
||||
usernameTemplate *template.Template `yaml:"-"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
communityTemplate *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) setDefaults() {
|
||||
|
@ -95,7 +97,18 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
}
|
||||
|
||||
bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(bc.CommunityTemplate) > 0 {
|
||||
bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UsernameTemplateArgs struct {
|
||||
|
@ -128,6 +141,21 @@ func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
type CommunityTemplateArgs struct {
|
||||
Localpart string
|
||||
Server string
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) EnableCommunities() bool {
|
||||
return bc.communityTemplate != nil
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatCommunity(localpart, server string) string {
|
||||
var buf bytes.Buffer
|
||||
bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server})
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type PermissionConfig map[string]PermissionLevel
|
||||
|
||||
type PermissionLevel int
|
||||
|
|
12
database/upgrades/2019-08-10-portal-in-community-field.go
Normal file
12
database/upgrades/2019-08-10-portal-in-community-field.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package upgrades
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
upgrades[8] = upgrade{"Add columns to store portal in filtering community meta", func(dialect Dialect, tx *sql.Tx, db *sql.DB) error {
|
||||
_, err := tx.Exec(`ALTER TABLE user_portal ADD COLUMN in_community BOOLEAN NOT NULL DEFAULT FALSE`)
|
||||
return err
|
||||
}}
|
||||
}
|
|
@ -22,7 +22,7 @@ type upgrade struct {
|
|||
fn upgradeFunc
|
||||
}
|
||||
|
||||
const NumberOfUpgrades = 8
|
||||
const NumberOfUpgrades = 9
|
||||
|
||||
var upgrades [NumberOfUpgrades]upgrade
|
||||
|
||||
|
|
|
@ -166,7 +166,12 @@ func (user *User) Update() {
|
|||
}
|
||||
}
|
||||
|
||||
func (user *User) SetPortalKeys(newKeys []PortalKey) error {
|
||||
type PortalKeyWithMeta struct {
|
||||
PortalKey
|
||||
InCommunity bool
|
||||
}
|
||||
|
||||
func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error {
|
||||
tx, err := user.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -177,14 +182,16 @@ func (user *User) SetPortalKeys(newKeys []PortalKey) error {
|
|||
return err
|
||||
}
|
||||
valueStrings := make([]string, len(newKeys))
|
||||
values := make([]interface{}, len(newKeys)*3)
|
||||
values := make([]interface{}, len(newKeys)*4)
|
||||
for i, key := range newKeys {
|
||||
valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d)", i*3+1, i*3+2, i*3+3)
|
||||
values[i*3] = user.jidPtr()
|
||||
values[i*3+1] = key.JID
|
||||
values[i*3+2] = key.Receiver
|
||||
pos := i * 4
|
||||
valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
|
||||
values[pos] = user.jidPtr()
|
||||
values[pos+1] = key.JID
|
||||
values[pos+2] = key.Receiver
|
||||
values[pos+3] = key.InCommunity
|
||||
}
|
||||
query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver) VALUES %s",
|
||||
query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s",
|
||||
strings.Join(valueStrings, ", "))
|
||||
_, err = tx.Exec(query, values...)
|
||||
if err != nil {
|
||||
|
@ -212,3 +219,23 @@ func (user *User) GetPortalKeys() []PortalKey {
|
|||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (user *User) GetInCommunityMap() map[PortalKey]bool {
|
||||
rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr())
|
||||
if err != nil {
|
||||
user.log.Warnln("Failed to get user portal keys:", err)
|
||||
return nil
|
||||
}
|
||||
keys := make(map[PortalKey]bool)
|
||||
for rows.Next() {
|
||||
var key PortalKey
|
||||
var inCommunity bool
|
||||
err = rows.Scan(&key.JID, &key.Receiver, &inCommunity)
|
||||
if err != nil {
|
||||
user.log.Warnln("Failed to scan row:", err)
|
||||
continue
|
||||
}
|
||||
keys[key] = inCommunity
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ bridge:
|
|||
# {{.Name}} - display name from contact list
|
||||
# {{.Short}} - short display name from contact list
|
||||
displayname_template: "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)"
|
||||
# Localpart template for per-user room grouping community IDs.
|
||||
# The bridge will create these communities and add all of the specific user's portals to the community.
|
||||
# {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user.
|
||||
community_template: whatsapp_{{.Localpart}}={{.Server}}
|
||||
|
||||
# WhatsApp connection timeout in seconds.
|
||||
connection_timeout: 20
|
||||
|
@ -104,7 +108,7 @@ bridge:
|
|||
# but causes room avatar/name bugs.
|
||||
private_chat_portal_meta: false
|
||||
|
||||
# Allow invite permission for user. User can invite any bots to room with whatsapp
|
||||
# Allow invite permission for user. User can invite any bots to room with whatsapp
|
||||
# users (private chat and groups)
|
||||
allow_user_invite: false
|
||||
|
||||
|
|
23
user.go
23
user.go
|
@ -29,12 +29,12 @@ import (
|
|||
"github.com/skip2/go-qrcode"
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/format"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/format"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/database"
|
||||
"maunium.net/go/mautrix-whatsapp/types"
|
||||
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||
|
@ -52,6 +52,7 @@ type User struct {
|
|||
Connected bool
|
||||
|
||||
ConnectionErrors int
|
||||
CommunityID string
|
||||
|
||||
cleanDisconnection bool
|
||||
|
||||
|
@ -183,7 +184,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
|
|||
conn, err := whatsapp.NewConn(timeout * time.Second)
|
||||
if err != nil {
|
||||
user.log.Errorln("Failed to connect to WhatsApp:", err)
|
||||
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. "+
|
||||
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. " +
|
||||
"This indicates a network problem on the bridge server. See bridge logs for more info.")
|
||||
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
||||
return false
|
||||
|
@ -202,7 +203,7 @@ func (user *User) RestoreSession() bool {
|
|||
return true
|
||||
} else if err != nil {
|
||||
user.log.Errorln("Failed to restore session:", err)
|
||||
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp "+
|
||||
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp " +
|
||||
"on your phone is reachable and use `reconnect` to try connecting again.")
|
||||
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
||||
return false
|
||||
|
@ -338,6 +339,7 @@ func (cl ChatList) Swap(i, j int) {
|
|||
}
|
||||
|
||||
func (user *User) PostLogin() {
|
||||
user.log.Debugln("Locking processing of incoming messages and starting post-login sync")
|
||||
user.syncLock.Lock()
|
||||
go user.intPostLogin()
|
||||
}
|
||||
|
@ -349,15 +351,18 @@ func (user *User) intPostLogin() {
|
|||
time.Sleep(dur)
|
||||
user.log.Debugfln("Waited %s, have %d chats and %d contacts", dur, len(user.Conn.Store.Chats), len(user.Conn.Store.Contacts))
|
||||
|
||||
user.createCommunity()
|
||||
go user.syncPuppets()
|
||||
user.syncPortals(false)
|
||||
user.log.Debugln("Post-login sync complete, unlocking processing of incoming messages")
|
||||
user.syncLock.Unlock()
|
||||
}
|
||||
|
||||
func (user *User) syncPortals(createAll bool) {
|
||||
user.log.Infoln("Reading chat list")
|
||||
chats := make(ChatList, 0, len(user.Conn.Store.Chats))
|
||||
portalKeys := make([]database.PortalKey, 0, len(user.Conn.Store.Chats))
|
||||
existingKeys := user.GetInCommunityMap()
|
||||
portalKeys := make([]database.PortalKeyWithMeta, 0, len(user.Conn.Store.Chats))
|
||||
for _, chat := range user.Conn.Store.Chats {
|
||||
ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64)
|
||||
if err != nil {
|
||||
|
@ -371,7 +376,11 @@ func (user *User) syncPortals(createAll bool) {
|
|||
Contact: user.Conn.Store.Contacts[chat.Jid],
|
||||
LastMessageTime: ts,
|
||||
})
|
||||
portalKeys = append(portalKeys, portal.Key)
|
||||
var inCommunity, ok bool
|
||||
if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity {
|
||||
inCommunity = user.addPortalToCommunity(portal)
|
||||
}
|
||||
portalKeys = append(portalKeys, database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
|
||||
}
|
||||
user.log.Infoln("Read chat list, updating user-portal mapping")
|
||||
err := user.SetPortalKeys(portalKeys)
|
||||
|
|
Loading…
Reference in a new issue