Add portal rooms to user-specific community for filtering

This commit is contained in:
Tulir Asokan 2019-08-10 15:24:53 +03:00
parent 07b8936985
commit 7bf470d69e
7 changed files with 198 additions and 18 deletions

100
community.go Normal file
View 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
}

View file

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

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

View file

@ -22,7 +22,7 @@ type upgrade struct {
fn upgradeFunc
}
const NumberOfUpgrades = 8
const NumberOfUpgrades = 9
var upgrades [NumberOfUpgrades]upgrade

View file

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

View file

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

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