Move whatsapp-ext to go-whatsapp

This commit is contained in:
Tulir Asokan 2021-02-17 01:21:30 +02:00
parent ebf2072025
commit 69dd7f803a
40 changed files with 203 additions and 1732 deletions

View file

@ -35,7 +35,6 @@ import (
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
type CommandHandler struct {
@ -737,12 +736,12 @@ const cmdListHelp = `list <contacts|groups> [page] [items per page] - Get a list
func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
for jid, contact := range input {
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) != contacts {
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
continue
}
if contacts {
result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.Jid[:len(contact.Jid)-len(whatsappExt.NewUserSuffix)]))
result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.Jid[:len(contact.Jid)-len(whatsapp.NewUserSuffix)]))
} else {
result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.Jid))
}
@ -818,8 +817,8 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
user := ce.User
jid := ce.Args[0]
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsappExt.NewUserSuffix)])
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
return
}
@ -865,7 +864,7 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
return
}
}
jid := number + whatsappExt.NewUserSuffix
jid := number + whatsapp.NewUserSuffix
handler.log.Debugln("Importing", jid, "for", user)

View file

@ -26,8 +26,6 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types"
)
type BridgeConfig struct {
@ -185,7 +183,7 @@ func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8
return buf.String(), quality
}
func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
func (bc BridgeConfig) FormatUsername(userID whatsapp.JID) string {
var buf bytes.Buffer
bc.usernameTemplate.Execute(&buf, userID)
return buf.String()

View file

@ -22,11 +22,11 @@ import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
)
@ -54,18 +54,18 @@ func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
return
}
func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " +
func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content "+
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
}
func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " +
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content "+
"FROM message WHERE mxid=$1", mxid)
}
func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message {
msg := mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " +
msg := mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content "+
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 ORDER BY timestamp DESC LIMIT 1", chat.JID, chat.Receiver)
if msg == nil || msg.Timestamp == 0 {
// Old db, we don't know what the last message is.
@ -87,9 +87,9 @@ type Message struct {
log log.Logger
Chat PortalKey
JID types.WhatsAppMessageID
JID whatsapp.MessageID
MXID id.EventID
Sender types.WhatsAppID
Sender whatsapp.JID
Timestamp uint64
Content *waProto.Message
}
@ -134,7 +134,7 @@ func (msg *Message) encodeBinaryContent() []byte {
}
func (msg *Message) Insert() {
_, err := msg.db.Exec("INSERT INTO message (chat_jid, chat_receiver, jid, mxid, sender, timestamp, content) " +
_, err := msg.db.Exec("INSERT INTO message (chat_jid, chat_receiver, jid, mxid, sender, timestamp, content) "+
"VALUES ($1, $2, $3, $4, $5, $6, $7)",
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.Timestamp, msg.encodeBinaryContent())
if err != nil {

View file

@ -22,24 +22,24 @@ import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
)
type PortalKey struct {
JID types.WhatsAppID
Receiver types.WhatsAppID
JID whatsapp.JID
Receiver whatsapp.JID
}
func GroupPortalKey(jid types.WhatsAppID) PortalKey {
func GroupPortalKey(jid whatsapp.JID) PortalKey {
return PortalKey{
JID: jid,
Receiver: jid,
}
}
func NewPortalKey(jid, receiver types.WhatsAppID) PortalKey {
func NewPortalKey(jid, receiver whatsapp.JID) PortalKey {
if strings.HasSuffix(jid, "@g.us") {
receiver = jid
}
@ -80,11 +80,11 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
}
func (pq *PortalQuery) GetAllByJID(jid types.WhatsAppID) []*Portal {
func (pq *PortalQuery) GetAllByJID(jid whatsapp.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
}
func (pq *PortalQuery) FindPrivateChats(receiver types.WhatsAppID) []*Portal {
func (pq *PortalQuery) FindPrivateChats(receiver whatsapp.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver)
}

View file

@ -21,9 +21,9 @@ import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
)
type PuppetQuery struct {
@ -53,7 +53,7 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
return
}
func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet {
func (pq *PuppetQuery) Get(jid whatsapp.JID) *Puppet {
row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE jid=$1", jid)
if row == nil {
return nil
@ -85,7 +85,7 @@ type Puppet struct {
db *Database
log log.Logger
JID types.WhatsAppID
JID whatsapp.JID
Avatar string
AvatarURL id.ContentURI
Displayname string

View file

@ -26,8 +26,6 @@ import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
"maunium.net/go/mautrix/id"
)
@ -63,7 +61,7 @@ func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
return uq.New().Scan(row)
}
func (uq *UserQuery) GetByJID(userID types.WhatsAppID) *User {
func (uq *UserQuery) GetByJID(userID whatsapp.JID) *User {
row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE jid=$1`, stripSuffix(userID))
if row == nil {
return nil
@ -76,7 +74,7 @@ type User struct {
log log.Logger
MXID id.UserID
JID types.WhatsAppID
JID whatsapp.JID
ManagementRoom id.RoomID
Session *whatsapp.Session
LastConnection uint64
@ -93,14 +91,14 @@ func (user *User) Scan(row Scannable) *User {
return nil
}
if len(jid.String) > 0 && len(clientID.String) > 0 {
user.JID = jid.String + whatsappExt.NewUserSuffix
user.JID = jid.String + whatsapp.NewUserSuffix
user.Session = &whatsapp.Session{
ClientId: clientID.String,
ClientToken: clientToken.String,
ServerToken: serverToken.String,
EncKey: encKey,
MacKey: macKey,
Wid: jid.String + whatsappExt.OldUserSuffix,
Wid: jid.String + whatsapp.OldUserSuffix,
}
} else {
user.Session = nil
@ -108,7 +106,7 @@ func (user *User) Scan(row Scannable) *User {
return user
}
func stripSuffix(jid types.WhatsAppID) string {
func stripSuffix(jid whatsapp.JID) string {
if len(jid) == 0 {
return jid
}

View file

@ -22,12 +22,11 @@ import (
"regexp"
"strings"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
@ -58,9 +57,9 @@ func NewFormatter(bridge *Bridge) *Formatter {
if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
if puppet != nil {
jids, ok := ctx[mentionedJIDsContextKey].([]types.WhatsAppID)
jids, ok := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
if !ok {
ctx[mentionedJIDsContextKey] = []types.WhatsAppID{puppet.JID}
ctx[mentionedJIDsContextKey] = []whatsapp.JID{puppet.JID}
} else {
ctx[mentionedJIDsContextKey] = append(jids, puppet.JID)
}
@ -105,7 +104,7 @@ func NewFormatter(bridge *Bridge) *Formatter {
return formatter
}
func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid id.UserID, displayname string) {
func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID, displayname string) {
if user := formatter.bridge.GetUserByJID(jid); user != nil {
mxid = user.MXID
displayname = string(user.MXID)
@ -116,7 +115,7 @@ func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid id.Us
return
}
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []types.WhatsAppID) {
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []whatsapp.JID) {
output := html.EscapeString(content.Body)
for regex, replacement := range formatter.waReplString {
output = regex.ReplaceAllString(output, replacement)
@ -126,7 +125,7 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
}
for _, jid := range mentionedJIDs {
mxid, displayname := formatter.getMatrixInfoByJID(jid)
number := "@" + strings.Replace(jid, whatsappExt.NewUserSuffix, "", 1)
number := "@" + strings.Replace(jid, whatsapp.NewUserSuffix, "", 1)
output = strings.Replace(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname), -1)
content.Body = strings.Replace(content.Body, number, displayname, -1)
}
@ -140,9 +139,9 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
}
}
func (formatter *Formatter) ParseMatrix(html string) (string, []types.WhatsAppID) {
func (formatter *Formatter) ParseMatrix(html string) (string, []whatsapp.JID) {
ctx := make(format.Context)
result := formatter.matrixHTMLParser.Parse(html, ctx)
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]types.WhatsAppID)
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
return result, mentionedJIDs
}

View file

@ -1,2 +0,0 @@
[*.{yaml,yml}]
indent_size = 2

View file

@ -1,2 +0,0 @@
charts/*
!*.yaml

View file

@ -1,22 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View file

@ -1,14 +0,0 @@
apiVersion: v1
name: mautrix-whatsapp
version: 0.1.0
appVersion: "0.1.0"
description: A Matrix-Whatsapp puppeting bridge.
keywords:
- matrix
- bridge
- whatsapp
maintainers:
- name: Tulir Asokan
email: tulir@maunium.net
sources:
- https://github.com/tulir/mautrix-whatsapp

View file

@ -1,6 +0,0 @@
dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/
version: 6.5.0
digest: sha256:85139e9d4207e49c11c5f84d7920d0135cffd3d427f3f3638d4e51258990de2a
generated: "2019-10-23T22:11:37.005827507+03:00"

View file

@ -1,5 +0,0 @@
dependencies:
- name: postgresql
version: 6.5.0
repository: https://kubernetes-charts.storage.googleapis.com/
condition: postgresql.enabled

View file

@ -1,12 +0,0 @@
Your registration file is below. Save it into a YAML file and give the path to that file to synapse:
id: {{ .Values.appservice.id }}
as_token: {{ .Values.appservice.asToken }}
hs_token: {{ .Values.appservice.hsToken }}
namespaces:
users:
- exclusive: true
regex: "@{{ .Values.bridge.username_template | replace "{{.}}" ".+"}}:{{ .Values.homeserver.domain }}"
url: {{ .Values.appservice.address }}
sender_localpart: {{ .Values.appservice.botUsername }}
rate_limited: false

View file

@ -1,55 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "mautrix-whatsapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "mautrix-whatsapp.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mautrix-whatsapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "mautrix-whatsapp.labels" -}}
app.kubernetes.io/name: {{ include "mautrix-whatsapp.name" . }}
helm.sh/chart: {{ include "mautrix-whatsapp.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "mautrix-whatsapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "mautrix-whatsapp.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View file

@ -1,45 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "mautrix-whatsapp.fullname" . }}
labels:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ template "mautrix-whatsapp.name" . }}
data:
config.yaml: |
homeserver:
address: {{ .Values.homeserver.address }}
domain: {{ .Values.homeserver.domain }}
appservice:
address: http://{{ include "mautrix-whatsapp.fullname" . }}:{{ .Values.service.port }}
hostname: 0.0.0.0
port: {{ .Values.service.port }}
{{- if .Values.postgresql.enabled }}
database:
type: postgres
uri: "postgres://postgres:{{ .Values.postgresql.postgresqlPassword }}@{{ .Release.Name }}-postgresql/{{ .Values.postgresql.postgresqlDatabase }}?sslmode=disable"
{{- else }}
database:
{{- toYaml .Values.appservice.database | nindent 8 }}
{{- end }}
id: {{ .Values.appservice.id }}
bot:
username: {{ .Values.appservice.botUsername }}
displayname: {{ .Values.appservice.botDisplayname }}
avatar: {{ .Values.appservice.botAvatar }}
as_token: {{ .Values.appservice.asToken }}
hs_token: {{ .Values.appservice.hsToken }}
bridge:
{{- toYaml .Values.bridge | nindent 6 }}
logging:
{{- toYaml .Values.logging | nindent 6 }}
registration.yaml: ""

View file

@ -1,69 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mautrix-whatsapp.fullname" . }}
labels:
{{- include "mautrix-whatsapp.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ include "mautrix-whatsapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
{{- if .Values.podAnnotations }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 6 }}
{{- end }}
metadata:
labels:
app.kubernetes.io/name: {{ include "mautrix-whatsapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
serviceAccountName: {{ template "mautrix-whatsapp.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
volumeMounts:
- mountPath: /data
name: config-volume
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
# livenessProbe:
# httpGet:
# path: /_matrix/mau/live
# port: http
# initialDelaySeconds: 60
# periodSeconds: 5
# readinessProbe:
# httpGet:
# path: /_matrix/mau/ready
# port: http
# initialDelaySeconds: 60
# periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: config-volume
configMap:
name: {{ template "mautrix-whatsapp.fullname" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "mautrix-whatsapp.fullname" . }}
labels:
{{ include "mautrix-whatsapp.labels" . | indent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "mautrix-whatsapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

View file

@ -1,8 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "mautrix-whatsapp.serviceAccountName" . }}
labels:
{{ include "mautrix-whatsapp.labels" . | indent 4 }}
{{- end -}}

View file

@ -1,137 +0,0 @@
image:
repository: dock.mau.dev/tulir/mautrix-whatsapp
tag: latest
pullPolicy: IfNotPresent
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
service:
type: ClusterIP
port: 29318
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
# Postgres pod configs
postgresql:
enabled: true
postgresqlDatabase: mxwa
postgresqlPassword: SET TO RANDOM STRING
persistence:
size: 2Gi
resources:
requests:
memory: 256Mi
cpu: 100m
# Homeserver details
homeserver:
# The address that this appservice can use to connect to the homeserver.
address: https://example.com
# The domain of the homeserver (for MXIDs, etc).
domain: example.com
# Application service host/registration related details
# Changing these values requires regeneration of the registration.
appservice:
id: whatsapp
botUsername: whatsappbot
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
botDisplayname: WhatsApp bridge bot
botAvatar: mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr
# Authentication tokens for AS <-> HS communication.
asToken: SET TO RANDOM STRING
hsToken: SET TO RANDOM STRING
# The keys below can be used to override the configs in the base config:
# https://github.com/tulir/mautrix-whatsapp/blob/master/example-config.yaml
# Note that the "appservice" and "homeserver" sections are above and slightly different than the base.
# Bridge config
bridge:
# Localpart template of MXIDs for WhatsApp users.
# {{.}} is replaced with the phone number of the WhatsApp user.
username_template: whatsapp_{{.}}
# Number of chats to sync for new users.
initial_chat_sync_count: 10
# Number of old messages to fill when creating new portal rooms.
initial_history_fill_count: 20
# Maximum number of chats to sync when recovering from downtime.
# Set to -1 to sync all new chats during downtime.
recovery_chat_sync_limit: -1
# Whether or not to sync history when recovering from downtime.
recovery_history_backfill: true
# Maximum number of seconds since last message in chat to skip
# syncing the chat in any case. This setting will take priority
# over both recovery_chat_sync_limit and initial_chat_sync_count.
# Default is 3 days = 259200 seconds
sync_max_chat_age: 259200
# Whether or not to explicitly set the avatar and room name for private
# chat portal rooms. This can be useful if the previous field works fine,
# but causes room avatar/name bugs.
private_chat_portal_meta: true
# Allow invite permission for user. User can invite any bots to room with whatsapp
# users (private chat and groups)
allow_user_invite: true
# Permissions for using the bridge.
# Permitted values:
# relaybot - Talk through the relaybot (if enabled), no access otherwise
# user - Access to use the bridge to chat with a WhatsApp account.
# admin - User level and some additional administration tools
# Permitted keys:
# * - All Matrix users
# domain - All users on that homeserver
# mxid - Specific user
permissions:
"*": relaybot
"example.com": user
"@admin:example.com": admin
relaybot:
# Whether or not relaybot support is enabled.
enabled: false
# The management room for the bot. This is where all status notifications are posted and
# in this room, you can use `!wa <command>` instead of `!wa relaybot <command>`. Omitting
# the command prefix completely like in user management rooms is not possible.
management: !foo:example.com
# List of users to invite to all created rooms that include the relaybot.
invites: []
# The formats to use when sending messages to WhatsApp via the relaybot.
message_formats:
m.text: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
m.notice: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
m.emote: "* <b>{{ .Sender.Displayname }}</b> {{ .Message }}"
m.file: "<b>{{ .Sender.Displayname }}</b> sent a file"
m.image: "<b>{{ .Sender.Displayname }}</b> sent an image"
m.audio: "<b>{{ .Sender.Displayname }}</b> sent an audio file"
m.video: "<b>{{ .Sender.Displayname }}</b> sent a video"
m.location: "<b>{{ .Sender.Displayname }}</b> sent a location"
logging:
timestamp_format: Jan _2, 2006 15:04:05
print_level: debug

11
main.go
View file

@ -25,6 +25,8 @@ import (
"syscall"
"time"
"github.com/Rhymen/go-whatsapp"
flag "maunium.net/go/mauflag"
log "maunium.net/go/maulogger/v2"
@ -36,7 +38,6 @@ import (
"maunium.net/go/mautrix-whatsapp/config"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/database/upgrades"
"maunium.net/go/mautrix-whatsapp/types"
)
var (
@ -137,14 +138,14 @@ type Bridge struct {
Metrics *MetricsHandler
usersByMXID map[id.UserID]*User
usersByJID map[types.WhatsAppID]*User
usersByJID map[whatsapp.JID]*User
usersLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
portalsByMXID map[id.RoomID]*Portal
portalsByJID map[database.PortalKey]*Portal
portalsLock sync.Mutex
puppets map[types.WhatsAppID]*Puppet
puppets map[whatsapp.JID]*Puppet
puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex
}
@ -163,11 +164,11 @@ type Crypto interface {
func NewBridge() *Bridge {
bridge := &Bridge{
usersByMXID: make(map[id.UserID]*User),
usersByJID: make(map[types.WhatsAppID]*User),
usersByJID: make(map[whatsapp.JID]*User),
managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByJID: make(map[database.PortalKey]*Portal),
puppets: make(map[types.WhatsAppID]*Puppet),
puppets: make(map[whatsapp.JID]*Puppet),
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
}

View file

@ -254,6 +254,10 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
return
}
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsConnected() {
return

View file

@ -27,11 +27,12 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
)
type MetricsHandler struct {
@ -56,11 +57,11 @@ type MetricsHandler struct {
unencryptedPrivateCount prometheus.Gauge
connected prometheus.Gauge
connectedState map[types.WhatsAppID]bool
connectedState map[whatsapp.JID]bool
loggedIn prometheus.Gauge
loggedInState map[types.WhatsAppID]bool
loggedInState map[whatsapp.JID]bool
syncLocked prometheus.Gauge
syncLockedState map[types.WhatsAppID]bool
syncLockedState map[whatsapp.JID]bool
bufferLength *prometheus.GaugeVec
}
@ -109,17 +110,17 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
Name: "bridge_logged_in",
Help: "Users logged into the bridge",
}),
loggedInState: make(map[types.WhatsAppID]bool),
loggedInState: make(map[whatsapp.JID]bool),
connected: promauto.NewGauge(prometheus.GaugeOpts{
Name: "bridge_connected",
Help: "Bridge users connected to WhatsApp",
}),
connectedState: make(map[types.WhatsAppID]bool),
connectedState: make(map[whatsapp.JID]bool),
syncLocked: promauto.NewGauge(prometheus.GaugeOpts{
Name: "bridge_sync_locked",
Help: "Bridge users locked in post-login sync",
}),
syncLockedState: make(map[types.WhatsAppID]bool),
syncLockedState: make(map[whatsapp.JID]bool),
bufferLength: promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "bridge_buffer_size",
Help: "Number of messages in buffer",
@ -149,7 +150,7 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
}
func (mh *MetricsHandler) TrackLoginState(jid types.WhatsAppID, loggedIn bool) {
func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
if !mh.running {
return
}
@ -164,7 +165,7 @@ func (mh *MetricsHandler) TrackLoginState(jid types.WhatsAppID, loggedIn bool) {
}
}
func (mh *MetricsHandler) TrackConnectionState(jid types.WhatsAppID, connected bool) {
func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool) {
if !mh.running {
return
}
@ -179,7 +180,7 @@ func (mh *MetricsHandler) TrackConnectionState(jid types.WhatsAppID, connected b
}
}
func (mh *MetricsHandler) TrackSyncLock(jid types.WhatsAppID, locked bool) {
func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
if !mh.running {
return
}

View file

@ -40,23 +40,19 @@ import (
"sync"
"time"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/crypto/attachment"
"github.com/Rhymen/go-whatsapp"
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
func (bridge *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
@ -83,7 +79,7 @@ func (bridge *Bridge) GetAllPortals() []*Portal {
return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAll())
}
func (bridge *Bridge) GetAllPortalsByJID(jid types.WhatsAppID) []*Portal {
func (bridge *Bridge) GetAllPortalsByJID(jid whatsapp.JID) []*Portal {
return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAllByJID(jid))
}
@ -131,7 +127,7 @@ func (bridge *Bridge) NewManualPortal(key database.PortalKey) *Portal {
bridge: bridge,
log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", key)),
recentlyHandled: [recentlyHandledLength]types.WhatsAppMessageID{},
recentlyHandled: [recentlyHandledLength]whatsapp.MessageID{},
messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer),
}
@ -146,7 +142,7 @@ func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
bridge: bridge,
log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
recentlyHandled: [recentlyHandledLength]types.WhatsAppMessageID{},
recentlyHandled: [recentlyHandledLength]whatsapp.MessageID{},
messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer),
}
@ -171,7 +167,7 @@ type Portal struct {
roomCreateLock sync.Mutex
recentlyHandled [recentlyHandledLength]types.WhatsAppMessageID
recentlyHandled [recentlyHandledLength]whatsapp.MessageID
recentlyHandledLock sync.Mutex
recentlyHandledIndex uint8
@ -256,7 +252,7 @@ func (portal *Portal) handleMessage(msg PortalMessage, isBackfill bool) {
portal.HandleLocationMessage(msg.source, data)
case whatsapp.StubMessage:
portal.HandleStubMessage(msg.source, data, isBackfill)
case whatsappExt.MessageRevocation:
case whatsapp.MessageRevocation:
portal.HandleMessageRevoke(msg.source, data)
case FakeMessage:
portal.HandleFakeMessage(msg.source, data)
@ -265,7 +261,7 @@ func (portal *Portal) handleMessage(msg PortalMessage, isBackfill bool) {
}
}
func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool {
func (portal *Portal) isRecentlyHandled(id whatsapp.MessageID) bool {
start := portal.recentlyHandledIndex
for i := start; i != start; i = (i - 1) % recentlyHandledLength {
if portal.recentlyHandled[i] == id {
@ -275,7 +271,7 @@ func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool {
return false
}
func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool {
func (portal *Portal) isDuplicate(id whatsapp.MessageID) bool {
msg := portal.bridge.DB.Message.GetByJID(portal.Key, id)
if msg != nil {
return true
@ -355,7 +351,7 @@ func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageIn
portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid)
}
func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
func (portal *Portal) SyncParticipants(metadata *whatsapp.GroupInfo) {
changed := false
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
if err != nil {
@ -413,7 +409,7 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) {
}
}
func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInfo, updateInfo bool) bool {
func (portal *Portal) UpdateAvatar(user *User, avatar *whatsapp.ProfilePicInfo, updateInfo bool) bool {
if avatar == nil || (avatar.Status == 0 && avatar.Tag != "remove" && len(avatar.URL) == 0) {
var err error
avatar, err = user.Conn.GetProfilePicThumb(portal.Key.JID)
@ -464,7 +460,7 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf
return true
}
func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool {
func (portal *Portal) UpdateName(name string, setBy whatsapp.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
if portal.Name != name {
portal.log.Debugfln("Updating name %s -> %s", portal.Name, name)
portal.Name = name
@ -488,7 +484,7 @@ func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, intent *ap
return false
}
func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool {
func (portal *Portal) UpdateTopic(topic string, setBy whatsapp.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
if portal.Topic != topic {
portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic)
portal.Topic = topic
@ -974,7 +970,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.log.Infoln("Creating Matrix room. Info source:", user.MXID)
var metadata *whatsappExt.GroupInfo
var metadata *whatsapp.GroupInfo
if portal.IsPrivateChat() {
puppet := portal.bridge.GetPuppetByJID(portal.Key.JID)
if portal.bridge.Config.Bridge.PrivateChatPortalMeta {
@ -1099,7 +1095,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
func (portal *Portal) IsPrivateChat() bool {
if portal.isPrivate == nil {
val := strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix)
val := strings.HasSuffix(portal.Key.JID, whatsapp.NewUserSuffix)
portal.isPrivate = &val
}
return *portal.isPrivate
@ -1152,7 +1148,7 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp
return
}
func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) {
func (portal *Portal) HandleMessageRevoke(user *User, message whatsapp.MessageRevocation) {
msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id)
if msg == nil || msg.IsFakeMXID() {
return
@ -1794,7 +1790,7 @@ func (portal *Portal) convertGifToVideo(gif []byte) ([]byte, error) {
func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsapp.MediaType) *MediaUpload {
var caption string
var mentionedJIDs []types.WhatsAppID
var mentionedJIDs []whatsapp.JID
if relaybotFormatted {
caption, mentionedJIDs = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
}
@ -1851,7 +1847,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
type MediaUpload struct {
Caption string
MentionedJIDs []types.WhatsAppID
MentionedJIDs []whatsapp.JID
URL string
MediaKey []byte
FileEncSHA256 []byte

View file

@ -27,12 +27,11 @@ import (
"time"
"github.com/gorilla/websocket"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix/id"
whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type ProvisioningAPI struct {
@ -433,7 +432,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
}
user.log.Debugln("Successful login via provisioning API")
user.ConnectionErrors = 0
user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
user.JID = strings.Replace(user.Conn.Info.Wid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
user.addToJIDMap()
user.SetSession(&session)
_ = c.WriteJSON(map[string]interface{}{

View file

@ -22,21 +22,18 @@ import (
"regexp"
"strings"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
var userIDRegex *regexp.Regexp
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.WhatsAppID, bool) {
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (whatsapp.JID, bool) {
if userIDRegex == nil {
userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
bridge.Config.Bridge.FormatUsername("([0-9]+)"),
@ -47,7 +44,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.WhatsAppID, bool) {
return "", false
}
jid := types.WhatsAppID(match[1] + whatsappExt.NewUserSuffix)
jid := whatsapp.JID(match[1] + whatsapp.NewUserSuffix)
return jid, true
}
@ -60,7 +57,7 @@ func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
return bridge.GetPuppetByJID(jid)
}
func (bridge *Bridge) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
func (bridge *Bridge) GetPuppetByJID(jid whatsapp.JID) *Puppet {
bridge.puppetsLock.Lock()
defer bridge.puppetsLock.Unlock()
puppet, ok := bridge.puppets[jid]
@ -125,12 +122,12 @@ func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet
return output
}
func (bridge *Bridge) FormatPuppetMXID(jid types.WhatsAppID) id.UserID {
func (bridge *Bridge) FormatPuppetMXID(jid whatsapp.JID) id.UserID {
return id.NewUserID(
bridge.Config.Bridge.FormatUsername(
strings.Replace(
jid,
whatsappExt.NewUserSuffix, "", 1)),
whatsapp.NewUserSuffix, "", 1)),
bridge.Config.Homeserver.Domain)
}
@ -161,7 +158,7 @@ type Puppet struct {
}
func (puppet *Puppet) PhoneNumber() string {
return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1)
return strings.Replace(puppet.JID, whatsapp.NewUserSuffix, "", 1)
}
func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
@ -181,7 +178,7 @@ func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
return puppet.bridge.AS.Intent(puppet.MXID)
}
func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicInfo) bool {
func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsapp.ProfilePicInfo) bool {
if avatar == nil {
var err error
avatar, err = source.Conn.GetProfilePicThumb(puppet.JID)

View file

@ -1,23 +0,0 @@
// 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 types
// WhatsAppID is a WhatsApp JID.
type WhatsAppID = string
// WhatsAppMessageID is the internal ID of a WhatsApp message.
type WhatsAppMessageID = string

238
user.go
View file

@ -40,13 +40,11 @@ import (
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
type User struct {
*database.User
Conn *whatsappExt.ExtendedConn
Conn *whatsapp.Conn
bridge *Bridge
log log.Logger
@ -91,7 +89,7 @@ func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User {
return user
}
func (bridge *Bridge) GetUserByJID(userID types.WhatsAppID) *User {
func (bridge *Bridge) GetUserByJID(userID whatsapp.JID) *User {
bridge.usersLock.Lock()
defer bridge.usersLock.Unlock()
user, ok := bridge.usersByJID[userID]
@ -264,7 +262,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
user.connLock.Unlock()
return false
}
user.Conn = whatsappExt.ExtendConn(conn)
user.Conn = conn
user.log.Debugln("WhatsApp connection successful")
user.Conn.AddHandler(user)
user.connLock.Unlock()
@ -419,7 +417,7 @@ func (user *User) Login(ce *CommandEvent) {
// TODO there's a bit of duplication between this and the provisioning API login method
// Also between the two logout methods (commands.go and provisioning.go)
user.ConnectionErrors = 0
user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
user.JID = strings.Replace(user.Conn.Info.Wid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
user.addToJIDMap()
user.SetSession(&session)
ce.Reply("Successfully logged in, synchronizing chats...")
@ -501,7 +499,7 @@ func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface
}
}
func (user *User) postConnPing(conn *whatsappExt.ExtendedConn) bool {
func (user *User) postConnPing(conn *whatsapp.Conn) bool {
if user.Conn != conn {
user.log.Warnln("Connection changed before scheduled post-connection ping, canceling ping")
return false
@ -528,7 +526,7 @@ func (user *User) postConnPing(conn *whatsappExt.ExtendedConn) bool {
}
}
func (user *User) intPostLogin(conn *whatsappExt.ExtendedConn) {
func (user *User) intPostLogin(conn *whatsapp.Conn) {
defer user.syncWait.Done()
user.lastReconnection = time.Now().Unix()
user.createCommunity()
@ -559,8 +557,60 @@ func (user *User) intPostLogin(conn *whatsappExt.ExtendedConn) {
}
}
func (user *User) HandleStreamEvent(evt whatsappExt.StreamEvent) {
if evt.Type == whatsappExt.StreamSleep {
type InfoGetter interface {
GetInfo() whatsapp.MessageInfo
}
func (user *User) HandleEvent(event interface{}) {
switch v := event.(type) {
case whatsapp.TextMessage, whatsapp.ImageMessage, whatsapp.StickerMessage, whatsapp.VideoMessage,
whatsapp.AudioMessage, whatsapp.DocumentMessage, whatsapp.ContactMessage, whatsapp.StubMessage,
whatsapp.LocationMessage:
info := v.(InfoGetter).GetInfo()
user.messageInput <- PortalMessage{info.RemoteJid, user, v, info.Timestamp}
case whatsapp.MessageRevocation:
user.messageInput <- PortalMessage{v.RemoteJid, user, v, 0}
case whatsapp.StreamEvent:
user.HandleStreamEvent(v)
case []whatsapp.Chat:
user.HandleChatList(v)
case []whatsapp.Contact:
user.HandleContactList(v)
case error:
user.HandleError(v)
case whatsapp.Contact:
go user.HandleNewContact(v)
case whatsapp.BatteryMessage:
user.HandleBatteryMessage(v)
case whatsapp.CallInfo:
user.HandleCallInfo(v)
case whatsapp.PresenceEvent:
go user.HandlePresence(v)
case whatsapp.JSONMsgInfo:
go user.HandleMsgInfo(v)
case whatsapp.ReceivedMessage:
user.HandleReceivedMessage(v)
case whatsapp.ReadMessage:
user.HandleReadMessage(v)
case whatsapp.JSONCommand:
user.HandleCommand(v)
case whatsapp.ChatUpdate:
user.HandleChatUpdate(v)
case json.RawMessage:
user.HandleJSONMessage(v)
case *waProto.WebMessageInfo:
user.updateLastConnectionIfNecessary()
// TODO trace log
user.log.Debugfln("WebMessageInfo: %+v", v)
case *waBinary.Node:
user.log.Debugfln("Unknown binary message: %+v", v)
default:
user.log.Debugfln("Unknown type of event in HandleEvent: %T", v)
}
}
func (user *User) HandleStreamEvent(evt whatsapp.StreamEvent) {
if evt.Type == whatsapp.StreamSleep {
if user.lastReconnection+60 > time.Now().Unix() {
user.lastReconnection = 0
user.log.Infoln("Stream went to sleep soon after reconnection, making new post-connection ping in 20 seconds")
@ -738,7 +788,7 @@ func (user *User) syncPuppets(contacts map[string]whatsapp.Contact) {
}
user.log.Infoln("Syncing puppet info from contacts")
for jid, contact := range contacts {
if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
puppet := user.bridge.GetPuppetByJID(contact.Jid)
puppet.Sync(user, contact)
}
@ -841,19 +891,11 @@ func (user *User) tryReconnect(msg string) {
}
}
func (user *User) ShouldCallSynchronously() bool {
return true
}
func (user *User) HandleJSONParseError(err error) {
user.log.Errorln("WhatsApp JSON parse error:", err)
}
func (user *User) PortalKey(jid types.WhatsAppID) database.PortalKey {
func (user *User) PortalKey(jid whatsapp.JID) database.PortalKey {
return database.NewPortalKey(jid, user.JID)
}
func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
func (user *User) GetPortalByJID(jid whatsapp.JID) *Portal {
return user.bridge.GetPortalByJID(user.PortalKey(jid))
}
@ -888,13 +930,11 @@ func (user *User) handleMessageLoop() {
func (user *User) HandleNewContact(contact whatsapp.Contact) {
user.log.Debugfln("Contact message: %+v", contact)
go func() {
if strings.HasSuffix(contact.Jid, whatsappExt.OldUserSuffix) {
contact.Jid = strings.Replace(contact.Jid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, -1)
}
puppet := user.bridge.GetPuppetByJID(contact.Jid)
puppet.UpdateName(user, contact)
}()
if strings.HasSuffix(contact.Jid, whatsapp.OldUserSuffix) {
contact.Jid = strings.Replace(contact.Jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1)
}
puppet := user.bridge.GetPuppetByJID(contact.Jid)
puppet.UpdateName(user, contact)
}
func (user *User) HandleBatteryMessage(battery whatsapp.BatteryMessage) {
@ -914,53 +954,13 @@ func (user *User) HandleBatteryMessage(battery whatsapp.BatteryMessage) {
}
}
func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleContactMessage(message whatsapp.ContactMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleStubMessage(message whatsapp.StubMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) {
user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
}
func (user *User) HandleMessageRevoke(message whatsappExt.MessageRevocation) {
user.messageInput <- PortalMessage{message.RemoteJid, user, message, 0}
}
type FakeMessage struct {
Text string
ID string
Alert bool
}
func (user *User) HandleCallInfo(info whatsappExt.CallInfo) {
func (user *User) HandleCallInfo(info whatsapp.CallInfo) {
if info.Data != nil {
return
}
@ -968,19 +968,19 @@ func (user *User) HandleCallInfo(info whatsappExt.CallInfo) {
ID: info.ID,
}
switch info.Type {
case whatsappExt.CallOffer:
case whatsapp.CallOffer:
if !user.bridge.Config.Bridge.CallNotices.Start {
return
}
data.Text = "Incoming call"
data.Alert = true
case whatsappExt.CallOfferVideo:
case whatsapp.CallOfferVideo:
if !user.bridge.Config.Bridge.CallNotices.Start {
return
}
data.Text = "Incoming video call"
data.Alert = true
case whatsappExt.CallTerminate:
case whatsapp.CallTerminate:
if !user.bridge.Config.Bridge.CallNotices.End {
return
}
@ -995,7 +995,7 @@ func (user *User) HandleCallInfo(info whatsappExt.CallInfo) {
}
}
func (user *User) HandlePresence(info whatsappExt.Presence) {
func (user *User) HandlePresence(info whatsapp.PresenceEvent) {
puppet := user.bridge.GetPuppetByJID(info.SenderJID)
switch info.Status {
case whatsapp.PresenceUnavailable:
@ -1023,33 +1023,31 @@ func (user *User) HandlePresence(info whatsappExt.Presence) {
}
}
func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) {
if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead {
func (user *User) HandleMsgInfo(info whatsapp.JSONMsgInfo) {
if (info.Command == whatsapp.MsgInfoCommandAck || info.Command == whatsapp.MsgInfoCommandAcks) && info.Acknowledgement == whatsapp.AckMessageRead {
portal := user.GetPortalByJID(info.ToJID)
if len(portal.MXID) == 0 {
return
}
go func() {
intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal)
for _, msgID := range info.IDs {
msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID)
if msg == nil || msg.IsFakeMXID() {
continue
}
err := intent.MarkRead(portal.MXID, msg.MXID)
if err != nil {
user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err)
}
intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal)
for _, msgID := range info.IDs {
msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID)
if msg == nil || msg.IsFakeMXID() {
continue
}
}()
err := intent.MarkRead(portal.MXID, msg.MXID)
if err != nil {
user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err)
}
}
}
}
func (user *User) HandleReceivedMessage(received whatsapp.ReceivedMessage) {
if received.Type == "read" {
user.markSelfRead(received.Jid, received.Index)
go user.markSelfRead(received.Jid, received.Index)
} else {
user.log.Debugfln("Unknown received message type: %+v", received)
}
@ -1057,12 +1055,12 @@ func (user *User) HandleReceivedMessage(received whatsapp.ReceivedMessage) {
func (user *User) HandleReadMessage(read whatsapp.ReadMessage) {
user.log.Debugfln("Received chat read message: %+v", read)
user.markSelfRead(read.Jid, "")
go user.markSelfRead(read.Jid, "")
}
func (user *User) markSelfRead(jid, messageID string) {
if strings.HasSuffix(jid, whatsappExt.OldUserSuffix) {
jid = strings.Replace(jid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, -1)
if strings.HasSuffix(jid, whatsapp.OldUserSuffix) {
jid = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1)
}
puppet := user.bridge.GetPuppetByJID(user.JID)
if puppet == nil {
@ -1096,17 +1094,17 @@ func (user *User) markSelfRead(jid, messageID string) {
}
}
func (user *User) HandleCommand(cmd whatsappExt.Command) {
func (user *User) HandleCommand(cmd whatsapp.JSONCommand) {
switch cmd.Type {
case whatsappExt.CommandPicture:
if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) {
case whatsapp.CommandPicture:
if strings.HasSuffix(cmd.JID, whatsapp.NewUserSuffix) {
puppet := user.bridge.GetPuppetByJID(cmd.JID)
go puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
} else if user.bridge.Config.Bridge.ChatMetaSync {
portal := user.GetPortalByJID(cmd.JID)
go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true)
}
case whatsappExt.CommandDisconnect:
case whatsapp.CommandDisconnect:
if cmd.Kind == "replaced" {
user.cleanDisconnection = true
go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" +
@ -1119,14 +1117,14 @@ func (user *User) HandleCommand(cmd whatsappExt.Command) {
}
}
func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
if cmd.Command != whatsappExt.ChatUpdateCommandAction {
func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) {
if cmd.Command != whatsapp.ChatUpdateCommandAction {
return
}
portal := user.GetPortalByJID(cmd.JID)
if len(portal.MXID) == 0 {
if cmd.Data.Action == whatsappExt.ChatActionIntroduce || cmd.Data.Action == whatsappExt.ChatActionCreate {
if cmd.Data.Action == whatsapp.ChatActionIntroduce || cmd.Data.Action == whatsapp.ChatActionCreate {
go func() {
err := portal.CreateMatrixRoom(user)
if err != nil {
@ -1139,11 +1137,11 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
// These don't come down the message history :(
switch cmd.Data.Action {
case whatsappExt.ChatActionAddTopic:
go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil,true)
case whatsappExt.ChatActionRemoveTopic:
go portal.UpdateTopic("", cmd.Data.SenderJID, nil,true)
case whatsappExt.ChatActionRemove:
case whatsapp.ChatActionAddTopic:
go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil, true)
case whatsapp.ChatActionRemoveTopic:
go portal.UpdateTopic("", cmd.Data.SenderJID, nil, true)
case whatsapp.ChatActionRemove:
// We ignore leaving groups in the message history to avoid accidentally leaving rejoined groups,
// but if we get a real-time command that says we left, it should be safe to bridge it.
if !user.bridge.Config.Bridge.ChatMetaSync {
@ -1162,45 +1160,35 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
}
switch cmd.Data.Action {
case whatsappExt.ChatActionNameChange:
case whatsapp.ChatActionNameChange:
go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true)
case whatsappExt.ChatActionPromote:
case whatsapp.ChatActionPromote:
go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
case whatsappExt.ChatActionDemote:
case whatsapp.ChatActionDemote:
go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false)
case whatsappExt.ChatActionAnnounce:
case whatsapp.ChatActionAnnounce:
go portal.RestrictMessageSending(cmd.Data.Announce)
case whatsappExt.ChatActionRestrict:
case whatsapp.ChatActionRestrict:
go portal.RestrictMetadataChanges(cmd.Data.Restrict)
case whatsappExt.ChatActionRemove:
case whatsapp.ChatActionRemove:
go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
case whatsappExt.ChatActionAdd:
case whatsapp.ChatActionAdd:
go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs)
case whatsappExt.ChatActionIntroduce:
case whatsapp.ChatActionIntroduce:
if cmd.Data.SenderJID != "unknown" {
go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID})
}
}
}
func (user *User) HandleJsonMessage(message string) {
var msg json.RawMessage
err := json.Unmarshal([]byte(message), &msg)
if err != nil {
func (user *User) HandleJSONMessage(message json.RawMessage) {
if !json.Valid(message) {
return
}
user.log.Debugln("JSON message:", message)
user.log.Debugfln("JSON message: %s", message)
user.updateLastConnectionIfNecessary()
}
func (user *User) HandleRawMessage(message *waProto.WebMessageInfo) {
user.updateLastConnectionIfNecessary()
}
func (user *User) HandleUnknownBinaryNode(node *waBinary.Node) {
user.log.Debugfln("Unknown binary message: %+v", node)
}
func (user *User) NeedsRelaybot(portal *Portal) bool {
return !user.HasSession() || !user.IsInPortal(portal.Key)
}

View file

@ -1,72 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type CallInfoType string
const (
CallOffer CallInfoType = "offer"
CallOfferVideo CallInfoType = "offer_video"
CallTransport CallInfoType = "transport"
CallRelayLatency CallInfoType = "relaylatency"
CallTerminate CallInfoType = "terminate"
)
type CallInfo struct {
ID string `json:"id"`
Type CallInfoType `json:"type"`
From string `json:"from"`
Platform string `json:"platform"`
Version []int `json:"version"`
Data [][]interface{} `json:"data"`
}
type CallInfoHandler interface {
whatsapp.Handler
HandleCallInfo(CallInfo)
}
func (ext *ExtendedConn) handleMessageCall(message []byte) {
var event CallInfo
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.From = strings.Replace(event.From, OldUserSuffix, NewUserSuffix, 1)
for _, handler := range ext.handlers {
callInfoHandler, ok := handler.(CallInfoHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(callInfoHandler) {
callInfoHandler.HandleCallInfo(event)
} else {
go callInfoHandler.HandleCallInfo(event)
}
}
}

View file

@ -1,183 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type ChatUpdateCommand string
const (
ChatUpdateCommandAction ChatUpdateCommand = "action"
)
type ChatUpdate struct {
JID string `json:"id"`
Command ChatUpdateCommand `json:"cmd"`
Data ChatUpdateData `json:"data"`
}
type ChatActionType string
const (
ChatActionNameChange ChatActionType = "subject"
ChatActionAddTopic ChatActionType = "desc_add"
ChatActionRemoveTopic ChatActionType = "desc_remove"
ChatActionRestrict ChatActionType = "restrict"
ChatActionAnnounce ChatActionType = "announce"
ChatActionPromote ChatActionType = "promote"
ChatActionDemote ChatActionType = "demote"
ChatActionIntroduce ChatActionType = "introduce"
ChatActionCreate ChatActionType = "create"
ChatActionRemove ChatActionType = "remove"
ChatActionAdd ChatActionType = "add"
)
type ChatUpdateData struct {
Action ChatActionType
SenderJID string
NameChange struct {
Name string `json:"subject"`
SetAt int64 `json:"s_t"`
SetBy string `json:"s_o"`
}
AddTopic struct {
Topic string `json:"desc"`
ID string `json:"descId"`
SetAt int64 `json:"descTime"`
SetBy string `json:"descOwner"`
}
RemoveTopic struct {
ID string `json:"descId"`
}
Introduce struct {
CreationTime int64 `json:"creation"`
Admins []string `json:"admins"`
SuperAdmins []string `json:"superadmins"`
Regulars []string `json:"regulars"`
}
Restrict bool
Announce bool
UserChange struct {
JIDs []string `json:"participants"`
}
}
func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error {
var arr []json.RawMessage
err := json.Unmarshal(data, &arr)
if err != nil {
return err
} else if len(arr) < 3 {
return nil
}
err = json.Unmarshal(arr[0], &cud.Action)
if err != nil {
return err
}
err = json.Unmarshal(arr[1], &cud.SenderJID)
if err != nil {
return err
}
cud.SenderJID = strings.Replace(cud.SenderJID, OldUserSuffix, NewUserSuffix, 1)
var unmarshalTo interface{}
switch cud.Action {
case ChatActionIntroduce, ChatActionCreate:
err = json.Unmarshal(arr[2], &cud.NameChange)
if err != nil {
return err
}
err = json.Unmarshal(arr[2], &cud.AddTopic)
if err != nil {
return err
}
unmarshalTo = &cud.Introduce
case ChatActionNameChange:
unmarshalTo = &cud.NameChange
case ChatActionAddTopic:
unmarshalTo = &cud.AddTopic
case ChatActionRemoveTopic:
unmarshalTo = &cud.RemoveTopic
case ChatActionRestrict:
unmarshalTo = &cud.Restrict
case ChatActionAnnounce:
unmarshalTo = &cud.Announce
case ChatActionPromote, ChatActionDemote, ChatActionRemove, ChatActionAdd:
unmarshalTo = &cud.UserChange
default:
return nil
}
err = json.Unmarshal(arr[2], unmarshalTo)
if err != nil {
return err
}
cud.NameChange.SetBy = strings.Replace(cud.NameChange.SetBy, OldUserSuffix, NewUserSuffix, 1)
for index, jid := range cud.UserChange.JIDs {
cud.UserChange.JIDs[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1)
}
for index, jid := range cud.Introduce.SuperAdmins {
cud.Introduce.SuperAdmins[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1)
}
for index, jid := range cud.Introduce.Admins {
cud.Introduce.Admins[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1)
}
for index, jid := range cud.Introduce.Regulars {
cud.Introduce.Regulars[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1)
}
return nil
}
type ChatUpdateHandler interface {
whatsapp.Handler
HandleChatUpdate(ChatUpdate)
}
func (ext *ExtendedConn) handleMessageChatUpdate(message []byte) {
var event ChatUpdate
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1)
for _, handler := range ext.handlers {
chatUpdateHandler, ok := handler.(ChatUpdateHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(chatUpdateHandler) {
chatUpdateHandler.HandleChatUpdate(event)
} else {
go chatUpdateHandler.HandleChatUpdate(event)
}
}
}

View file

@ -1,69 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type CommandType string
const (
CommandPicture CommandType = "picture"
CommandDisconnect CommandType = "disconnect"
)
type Command struct {
Type CommandType `json:"type"`
JID string `json:"jid"`
*ProfilePicInfo
Kind string `json:"kind"`
Raw json.RawMessage `json:"-"`
}
type CommandHandler interface {
whatsapp.Handler
HandleCommand(Command)
}
func (ext *ExtendedConn) handleMessageCommand(message []byte) {
var event Command
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.Raw = message
event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1)
for _, handler := range ext.handlers {
commandHandler, ok := handler.(CommandHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(commandHandler) {
commandHandler.HandleCommand(event)
} else {
go commandHandler.HandleCommand(event)
}
}
}

View file

@ -1,65 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"github.com/Rhymen/go-whatsapp"
)
type ConnInfo struct {
ProtocolVersion []int `json:"protoVersion"`
BinaryVersion int `json:"binVersion"`
Phone struct {
WhatsAppVersion string `json:"wa_version"`
MCC string `json:"mcc"`
MNC string `json:"mnc"`
OSVersion string `json:"os_version"`
DeviceManufacturer string `json:"device_manufacturer"`
DeviceModel string `json:"device_model"`
OSBuildNumber string `json:"os_build_number"`
} `json:"phone"`
Features map[string]interface{} `json:"features"`
PushName string `json:"pushname"`
}
type ConnInfoHandler interface {
whatsapp.Handler
HandleConnInfo(ConnInfo)
}
func (ext *ExtendedConn) handleMessageConn(message []byte) {
var event ConnInfo
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
for _, handler := range ext.handlers {
connInfoHandler, ok := handler.(ConnInfoHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(connInfoHandler) {
connInfoHandler.HandleConnInfo(event)
} else {
go connInfoHandler.HandleConnInfo(event)
}
}
}

View file

@ -1,68 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"fmt"
"maunium.net/go/mautrix-whatsapp/types"
)
type CreateGroupResponse struct {
Status int `json:"status"`
GroupID types.WhatsAppID `json:"gid"`
Participants map[types.WhatsAppID]struct {
Code string `json:"code"`
} `json:"participants"`
Source string `json:"-"`
}
type actualCreateGroupResponse struct {
Status int `json:"status"`
GroupID types.WhatsAppID `json:"gid"`
Participants []map[types.WhatsAppID]struct {
Code string `json:"code"`
} `json:"participants"`
}
func (ext *ExtendedConn) CreateGroup(subject string, participants []types.WhatsAppID) (*CreateGroupResponse, error) {
respChan, err := ext.Conn.CreateGroup(subject, participants)
if err != nil {
return nil, err
}
var resp CreateGroupResponse
var actualResp actualCreateGroupResponse
resp.Source = <-respChan
fmt.Println(">>>>>>", resp.Source)
err = json.Unmarshal([]byte(resp.Source), &actualResp)
if err != nil {
return nil, err
}
resp.Status = actualResp.Status
resp.GroupID = actualResp.GroupID
resp.Participants = make(map[types.WhatsAppID]struct {
Code string `json:"code"`
})
for _, participantMap := range actualResp.Participants {
for jid, status := range participantMap {
resp.Participants[jid] = status
}
}
return &resp, nil
}

View file

@ -1,105 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"github.com/Rhymen/go-whatsapp"
)
type JSONMessage []json.RawMessage
type JSONMessageType string
const (
MessageMsgInfo JSONMessageType = "MsgInfo"
MessageMsg JSONMessageType = "Msg"
MessagePresence JSONMessageType = "Presence"
MessageStream JSONMessageType = "Stream"
MessageConn JSONMessageType = "Conn"
MessageProps JSONMessageType = "Props"
MessageCmd JSONMessageType = "Cmd"
MessageChat JSONMessageType = "Chat"
MessageCall JSONMessageType = "Call"
)
func (ext *ExtendedConn) HandleError(error) {}
type UnhandledJSONMessageHandler interface {
whatsapp.Handler
HandleUnhandledJSONMessage(string)
}
type JSONParseErrorHandler interface {
whatsapp.Handler
HandleJSONParseError(error)
}
func (ext *ExtendedConn) jsonParseError(err error) {
for _, handler := range ext.handlers {
errorHandler, ok := handler.(JSONParseErrorHandler)
if !ok {
continue
}
errorHandler.HandleJSONParseError(err)
}
}
func (ext *ExtendedConn) HandleJsonMessage(message string) {
msg := JSONMessage{}
err := json.Unmarshal([]byte(message), &msg)
if err != nil || len(msg) < 2 {
ext.jsonParseError(err)
return
}
var msgType JSONMessageType
json.Unmarshal(msg[0], &msgType)
switch msgType {
case MessagePresence:
ext.handleMessagePresence(msg[1])
case MessageStream:
ext.handleMessageStream(msg[1:])
case MessageConn:
ext.handleMessageConn(msg[1])
case MessageProps:
ext.handleMessageProps(msg[1])
case MessageMsgInfo, MessageMsg:
ext.handleMessageMsgInfo(msgType, msg[1])
case MessageCmd:
ext.handleMessageCommand(msg[1])
case MessageChat:
ext.handleMessageChatUpdate(msg[1])
case MessageCall:
ext.handleMessageCall(msg[1])
default:
for _, handler := range ext.handlers {
ujmHandler, ok := handler.(UnhandledJSONMessageHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(ujmHandler) {
ujmHandler.HandleUnhandledJSONMessage(message)
} else {
go ujmHandler.HandleUnhandledJSONMessage(message)
}
}
}
}

View file

@ -1,95 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type MsgInfoCommand string
const (
MsgInfoCommandAck MsgInfoCommand = "ack"
MsgInfoCommandAcks MsgInfoCommand = "acks"
)
type Acknowledgement int
const (
AckMessageSent Acknowledgement = 1
AckMessageDelivered Acknowledgement = 2
AckMessageRead Acknowledgement = 3
)
type JSONStringOrArray []string
func (jsoa *JSONStringOrArray) UnmarshalJSON(data []byte) error {
var str string
if json.Unmarshal(data, &str) == nil {
*jsoa = []string{str}
return nil
}
var strs []string
json.Unmarshal(data, &strs)
*jsoa = strs
return nil
}
type MsgInfo struct {
Command MsgInfoCommand `json:"cmd"`
IDs JSONStringOrArray `json:"id"`
Acknowledgement Acknowledgement `json:"ack"`
MessageFromJID string `json:"from"`
SenderJID string `json:"participant"`
ToJID string `json:"to"`
Timestamp int64 `json:"t"`
}
type MsgInfoHandler interface {
whatsapp.Handler
HandleMsgInfo(MsgInfo)
}
func (ext *ExtendedConn) handleMessageMsgInfo(msgType JSONMessageType, message []byte) {
var event MsgInfo
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1)
event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1)
event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1)
if msgType == MessageMsg {
event.SenderJID = event.ToJID
}
for _, handler := range ext.handlers {
msgInfoHandler, ok := handler.(MsgInfoHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(msgInfoHandler) {
msgInfoHandler.HandleMsgInfo(event)
} else {
go msgInfoHandler.HandleMsgInfo(event)
}
}
}

View file

@ -1,64 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type Presence struct {
JID string `json:"id"`
SenderJID string `json:"participant"`
Status whatsapp.Presence `json:"type"`
Timestamp int64 `json:"t"`
Deny bool `json:"deny"`
}
type PresenceHandler interface {
whatsapp.Handler
HandlePresence(Presence)
}
func (ext *ExtendedConn) handleMessagePresence(message []byte) {
var event Presence
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1)
if len(event.SenderJID) == 0 {
event.SenderJID = event.JID
} else {
event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1)
}
for _, handler := range ext.handlers {
presenceHandler, ok := handler.(PresenceHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(presenceHandler) {
presenceHandler.HandlePresence(event)
} else {
go presenceHandler.HandlePresence(event)
}
}
}

View file

@ -1,73 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"github.com/Rhymen/go-whatsapp"
)
type ProtocolProps struct {
WebPresence bool `json:"webPresence"`
NotificationQuery bool `json:"notificationQuery"`
FacebookCrashLog bool `json:"fbCrashlog"`
Bucket string `json:"bucket"`
GIFSearch string `json:"gifSearch"`
Spam bool `json:"SPAM"`
SetBlock bool `json:"SET_BLOCK"`
MessageInfo bool `json:"MESSAGE_INFO"`
MaxFileSize int `json:"maxFileSize"`
Media int `json:"media"`
GroupNameLength int `json:"maxSubject"`
GroupDescriptionLength int `json:"groupDescLength"`
MaxParticipants int `json:"maxParticipants"`
VideoMaxEdge int `json:"videoMaxEdge"`
ImageMaxEdge int `json:"imageMaxEdge"`
ImageMaxKilobytes int `json:"imageMaxKBytes"`
Edit int `json:"edit"`
FwdUIStartTimestamp int `json:"fwdUiStartTs"`
GroupsV3 int `json:"groupsV3"`
RestrictGroups int `json:"restrictGroups"`
AnnounceGroups int `json:"announceGroups"`
}
type ProtocolPropsHandler interface {
whatsapp.Handler
HandleProtocolProps(ProtocolProps)
}
func (ext *ExtendedConn) handleMessageProps(message []byte) {
var event ProtocolProps
err := json.Unmarshal(message, &event)
if err != nil {
ext.jsonParseError(err)
return
}
for _, handler := range ext.handlers {
protocolPropsHandler, ok := handler.(ProtocolPropsHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(protocolPropsHandler) {
protocolPropsHandler.HandleProtocolProps(event)
} else {
go protocolPropsHandler.HandleProtocolProps(event)
}
}
}

View file

@ -1,59 +0,0 @@
// 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 whatsappExt
import (
"github.com/Rhymen/go-whatsapp"
"github.com/Rhymen/go-whatsapp/binary/proto"
)
type MessageRevokeHandler interface {
whatsapp.Handler
HandleMessageRevoke(key MessageRevocation)
}
type MessageRevocation struct {
Id string
RemoteJid string
FromMe bool
Participant string
}
func (ext *ExtendedConn) HandleRawMessage(message *proto.WebMessageInfo) {
protoMsg := message.GetMessage().GetProtocolMessage()
if protoMsg != nil && protoMsg.GetType() == proto.ProtocolMessage_REVOKE {
key := protoMsg.GetKey()
deletedMessage := MessageRevocation{
Id: key.GetId(),
RemoteJid: key.GetRemoteJid(),
FromMe: key.GetFromMe(),
Participant: key.GetParticipant(),
}
for _, handler := range ext.handlers {
mrHandler, ok := handler.(MessageRevokeHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(mrHandler) {
mrHandler.HandleMessageRevoke(deletedMessage)
} else {
go mrHandler.HandleMessageRevoke(deletedMessage)
}
}
}
}

View file

@ -1,76 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"github.com/Rhymen/go-whatsapp"
)
type StreamType string
const (
StreamUpdate = "update"
StreamSleep = "asleep"
)
type StreamEvent struct {
Type StreamType
IsOutdated bool
Version string
Extra []json.RawMessage
}
type StreamEventHandler interface {
whatsapp.Handler
HandleStreamEvent(StreamEvent)
}
func (ext *ExtendedConn) handleMessageStream(message []json.RawMessage) {
var event StreamEvent
err := json.Unmarshal(message[0], &event.Type)
if err != nil {
ext.jsonParseError(err)
return
}
if event.Type == StreamUpdate && len(message) >= 3 {
_ = json.Unmarshal(message[1], &event.IsOutdated)
_ = json.Unmarshal(message[2], &event.Version)
if len(message) >= 4 {
event.Extra = message[3:]
}
} else if len(message) >= 2 {
event.Extra = message[1:]
}
for _, handler := range ext.handlers {
streamHandler, ok := handler.(StreamEventHandler)
if !ok {
continue
}
if ext.shouldCallSynchronously(streamHandler) {
streamHandler.HandleStreamEvent(event)
} else {
go streamHandler.HandleStreamEvent(event)
}
}
}

View file

@ -1,164 +0,0 @@
// 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 whatsappExt
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/Rhymen/go-whatsapp"
)
const (
OldUserSuffix = "@c.us"
NewUserSuffix = "@s.whatsapp.net"
)
type ExtendedConn struct {
*whatsapp.Conn
handlers []whatsapp.Handler
}
func ExtendConn(conn *whatsapp.Conn) *ExtendedConn {
ext := &ExtendedConn{
Conn: conn,
}
ext.Conn.AddHandler(ext)
return ext
}
func (ext *ExtendedConn) AddHandler(handler whatsapp.Handler) {
ext.Conn.AddHandler(handler)
ext.handlers = append(ext.handlers, handler)
}
func (ext *ExtendedConn) RemoveHandler(handler whatsapp.Handler) bool {
ext.Conn.RemoveHandler(handler)
for i, v := range ext.handlers {
if v == handler {
ext.handlers = append(ext.handlers[:i], ext.handlers[i+1:]...)
return true
}
}
return false
}
func (ext *ExtendedConn) RemoveHandlers() {
ext.Conn.RemoveHandlers()
ext.handlers = make([]whatsapp.Handler, 0)
}
func (ext *ExtendedConn) shouldCallSynchronously(handler whatsapp.Handler) bool {
sh, ok := handler.(whatsapp.SyncHandler)
return ok && sh.ShouldCallSynchronously()
}
func (ext *ExtendedConn) ShouldCallSynchronously() bool {
return true
}
type GroupInfo struct {
JID string `json:"jid"`
OwnerJID string `json:"owner"`
Name string `json:"subject"`
NameSetTime int64 `json:"subjectTime"`
NameSetBy string `json:"subjectOwner"`
Announce bool `json:"announce"` // Can only admins send messages?
Topic string `json:"desc"`
TopicID string `json:"descId"`
TopicSetAt int64 `json:"descTime"`
TopicSetBy string `json:"descOwner"`
GroupCreated int64 `json:"creation"`
Status int16 `json:"status"`
Participants []struct {
JID string `json:"id"`
IsAdmin bool `json:"isAdmin"`
IsSuperAdmin bool `json:"isSuperAdmin"`
} `json:"participants"`
}
func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) {
data, err := ext.Conn.GetGroupMetaData(jid)
if err != nil {
return nil, fmt.Errorf("failed to get group metadata: %v", err)
}
content := <-data
info := &GroupInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal group metadata: %v", err)
}
for index, participant := range info.Participants {
info.Participants[index].JID = strings.Replace(participant.JID, OldUserSuffix, NewUserSuffix, 1)
}
info.NameSetBy = strings.Replace(info.NameSetBy, OldUserSuffix, NewUserSuffix, 1)
info.TopicSetBy = strings.Replace(info.TopicSetBy, OldUserSuffix, NewUserSuffix, 1)
return info, nil
}
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int `json:"status"`
}
func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) {
resp, err := http.Get(ppi.URL)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func (ppi *ProfilePicInfo) DownloadBytes() ([]byte, error) {
body, err := ppi.Download()
if err != nil {
return nil, err
}
defer body.Close()
data, err := ioutil.ReadAll(body)
return data, err
}
func (ext *ExtendedConn) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := ext.Conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}