Add portal rooms to user-specific community for filtering
This commit is contained in:
parent
07b8936985
commit
7bf470d69e
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 {
|
type BridgeConfig struct {
|
||||||
UsernameTemplate string `yaml:"username_template"`
|
UsernameTemplate string `yaml:"username_template"`
|
||||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||||
|
CommunityTemplate string `yaml:"community_template"`
|
||||||
|
|
||||||
ConnectionTimeout int `yaml:"connection_timeout"`
|
ConnectionTimeout int `yaml:"connection_timeout"`
|
||||||
LoginQRRegenCount int `yaml:"login_qr_regen_count"`
|
LoginQRRegenCount int `yaml:"login_qr_regen_count"`
|
||||||
@ -51,7 +52,7 @@ type BridgeConfig struct {
|
|||||||
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
|
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
|
||||||
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
|
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
|
||||||
|
|
||||||
AllowUserInvite bool `yaml:"allow_user_invite"`
|
AllowUserInvite bool `yaml:"allow_user_invite"`
|
||||||
|
|
||||||
CommandPrefix string `yaml:"command_prefix"`
|
CommandPrefix string `yaml:"command_prefix"`
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ type BridgeConfig struct {
|
|||||||
|
|
||||||
usernameTemplate *template.Template `yaml:"-"`
|
usernameTemplate *template.Template `yaml:"-"`
|
||||||
displaynameTemplate *template.Template `yaml:"-"`
|
displaynameTemplate *template.Template `yaml:"-"`
|
||||||
|
communityTemplate *template.Template `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BridgeConfig) setDefaults() {
|
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)
|
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 {
|
type UsernameTemplateArgs struct {
|
||||||
@ -128,6 +141,21 @@ func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
|
|||||||
return buf.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 PermissionConfig map[string]PermissionLevel
|
||||||
|
|
||||||
type PermissionLevel int
|
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
|
fn upgradeFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumberOfUpgrades = 8
|
const NumberOfUpgrades = 9
|
||||||
|
|
||||||
var upgrades [NumberOfUpgrades]upgrade
|
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()
|
tx, err := user.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -177,14 +182,16 @@ func (user *User) SetPortalKeys(newKeys []PortalKey) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
valueStrings := make([]string, len(newKeys))
|
valueStrings := make([]string, len(newKeys))
|
||||||
values := make([]interface{}, len(newKeys)*3)
|
values := make([]interface{}, len(newKeys)*4)
|
||||||
for i, key := range newKeys {
|
for i, key := range newKeys {
|
||||||
valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d)", i*3+1, i*3+2, i*3+3)
|
pos := i * 4
|
||||||
values[i*3] = user.jidPtr()
|
valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
|
||||||
values[i*3+1] = key.JID
|
values[pos] = user.jidPtr()
|
||||||
values[i*3+2] = key.Receiver
|
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, ", "))
|
strings.Join(valueStrings, ", "))
|
||||||
_, err = tx.Exec(query, values...)
|
_, err = tx.Exec(query, values...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -212,3 +219,23 @@ func (user *User) GetPortalKeys() []PortalKey {
|
|||||||
}
|
}
|
||||||
return keys
|
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
|
# {{.Name}} - display name from contact list
|
||||||
# {{.Short}} - short display name from contact list
|
# {{.Short}} - short display name from contact list
|
||||||
displayname_template: "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)"
|
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.
|
# WhatsApp connection timeout in seconds.
|
||||||
connection_timeout: 20
|
connection_timeout: 20
|
||||||
@ -104,7 +108,7 @@ bridge:
|
|||||||
# but causes room avatar/name bugs.
|
# but causes room avatar/name bugs.
|
||||||
private_chat_portal_meta: false
|
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)
|
# users (private chat and groups)
|
||||||
allow_user_invite: false
|
allow_user_invite: false
|
||||||
|
|
||||||
|
23
user.go
23
user.go
@ -29,12 +29,12 @@ import (
|
|||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
log "maunium.net/go/maulogger/v2"
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
|
|
||||||
"github.com/Rhymen/go-whatsapp"
|
"github.com/Rhymen/go-whatsapp"
|
||||||
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
|
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/database"
|
||||||
"maunium.net/go/mautrix-whatsapp/types"
|
"maunium.net/go/mautrix-whatsapp/types"
|
||||||
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
|
||||||
@ -52,6 +52,7 @@ type User struct {
|
|||||||
Connected bool
|
Connected bool
|
||||||
|
|
||||||
ConnectionErrors int
|
ConnectionErrors int
|
||||||
|
CommunityID string
|
||||||
|
|
||||||
cleanDisconnection bool
|
cleanDisconnection bool
|
||||||
|
|
||||||
@ -183,7 +184,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
|
|||||||
conn, err := whatsapp.NewConn(timeout * time.Second)
|
conn, err := whatsapp.NewConn(timeout * time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user.log.Errorln("Failed to connect to WhatsApp:", err)
|
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.")
|
"This indicates a network problem on the bridge server. See bridge logs for more info.")
|
||||||
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
||||||
return false
|
return false
|
||||||
@ -202,7 +203,7 @@ func (user *User) RestoreSession() bool {
|
|||||||
return true
|
return true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
user.log.Errorln("Failed to restore session:", err)
|
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.")
|
"on your phone is reachable and use `reconnect` to try connecting again.")
|
||||||
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg)
|
||||||
return false
|
return false
|
||||||
@ -338,6 +339,7 @@ func (cl ChatList) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) PostLogin() {
|
func (user *User) PostLogin() {
|
||||||
|
user.log.Debugln("Locking processing of incoming messages and starting post-login sync")
|
||||||
user.syncLock.Lock()
|
user.syncLock.Lock()
|
||||||
go user.intPostLogin()
|
go user.intPostLogin()
|
||||||
}
|
}
|
||||||
@ -349,15 +351,18 @@ func (user *User) intPostLogin() {
|
|||||||
time.Sleep(dur)
|
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.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()
|
go user.syncPuppets()
|
||||||
user.syncPortals(false)
|
user.syncPortals(false)
|
||||||
|
user.log.Debugln("Post-login sync complete, unlocking processing of incoming messages")
|
||||||
user.syncLock.Unlock()
|
user.syncLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) syncPortals(createAll bool) {
|
func (user *User) syncPortals(createAll bool) {
|
||||||
user.log.Infoln("Reading chat list")
|
user.log.Infoln("Reading chat list")
|
||||||
chats := make(ChatList, 0, len(user.Conn.Store.Chats))
|
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 {
|
for _, chat := range user.Conn.Store.Chats {
|
||||||
ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64)
|
ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -371,7 +376,11 @@ func (user *User) syncPortals(createAll bool) {
|
|||||||
Contact: user.Conn.Store.Contacts[chat.Jid],
|
Contact: user.Conn.Store.Contacts[chat.Jid],
|
||||||
LastMessageTime: ts,
|
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")
|
user.log.Infoln("Read chat list, updating user-portal mapping")
|
||||||
err := user.SetPortalKeys(portalKeys)
|
err := user.SetPortalKeys(portalKeys)
|
||||||
|
Loading…
Reference in New Issue
Block a user