slidge-whatsapp 0.2.0a0__cp311-cp311-manylinux_2_36_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of slidge-whatsapp might be problematic. Click here for more details.
- slidge_whatsapp/__init__.py +11 -0
- slidge_whatsapp/__main__.py +9 -0
- slidge_whatsapp/attachment.go +386 -0
- slidge_whatsapp/command.py +143 -0
- slidge_whatsapp/config.py +38 -0
- slidge_whatsapp/contact.py +75 -0
- slidge_whatsapp/event.go +856 -0
- slidge_whatsapp/gateway.go +175 -0
- slidge_whatsapp/gateway.py +97 -0
- slidge_whatsapp/generated/__init__.py +0 -0
- slidge_whatsapp/generated/_whatsapp.cpython-311-x86_64-linux-gnu.so +0 -0
- slidge_whatsapp/generated/build.py +378 -0
- slidge_whatsapp/generated/go.py +1720 -0
- slidge_whatsapp/generated/whatsapp.py +2797 -0
- slidge_whatsapp/go.mod +28 -0
- slidge_whatsapp/go.sum +55 -0
- slidge_whatsapp/group.py +240 -0
- slidge_whatsapp/session.go +783 -0
- slidge_whatsapp/session.py +663 -0
- slidge_whatsapp/util.py +12 -0
- slidge_whatsapp-0.2.0a0.dist-info/LICENSE +661 -0
- slidge_whatsapp-0.2.0a0.dist-info/METADATA +81 -0
- slidge_whatsapp-0.2.0a0.dist-info/RECORD +25 -0
- slidge_whatsapp-0.2.0a0.dist-info/WHEEL +4 -0
- slidge_whatsapp-0.2.0a0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
package whatsapp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
// Standard library.
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"runtime"
|
|
8
|
+
|
|
9
|
+
// Third-party libraries.
|
|
10
|
+
_ "github.com/mattn/go-sqlite3"
|
|
11
|
+
"go.mau.fi/whatsmeow/store"
|
|
12
|
+
"go.mau.fi/whatsmeow/store/sqlstore"
|
|
13
|
+
"go.mau.fi/whatsmeow/types"
|
|
14
|
+
walog "go.mau.fi/whatsmeow/util/log"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// A LinkedDevice represents a unique pairing session between the gateway and WhatsApp. It is not
|
|
18
|
+
// unique to the underlying "main" device (or phone number), as multiple linked devices may be paired
|
|
19
|
+
// with any main device.
|
|
20
|
+
type LinkedDevice struct {
|
|
21
|
+
// ID is an opaque string identifying this LinkedDevice to the Session. Noted that this string
|
|
22
|
+
// is currently equivalent to a password, and needs to be protected accordingly.
|
|
23
|
+
ID string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// JID returns the WhatsApp JID corresponding to the LinkedDevice ID. Empty or invalid device IDs
|
|
27
|
+
// may return invalid JIDs, and this function does not handle errors.
|
|
28
|
+
func (d LinkedDevice) JID() types.JID {
|
|
29
|
+
jid, _ := types.ParseJID(d.ID)
|
|
30
|
+
return jid
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A ErrorLevel is a value representing the severity of a log message being handled.
|
|
34
|
+
type ErrorLevel int
|
|
35
|
+
|
|
36
|
+
// The log levels handled by the overarching Session logger.
|
|
37
|
+
const (
|
|
38
|
+
LevelError ErrorLevel = 1 + iota
|
|
39
|
+
LevelWarning
|
|
40
|
+
LevelInfo
|
|
41
|
+
LevelDebug
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// HandleLogFunc is the signature for the overarching Gateway log handling function.
|
|
45
|
+
type HandleLogFunc func(ErrorLevel, string)
|
|
46
|
+
|
|
47
|
+
// Errorf handles the given message as representing a (typically) fatal error.
|
|
48
|
+
func (h HandleLogFunc) Errorf(msg string, args ...interface{}) {
|
|
49
|
+
h(LevelError, fmt.Sprintf(msg, args...))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Warn handles the given message as representing a non-fatal error or warning thereof.
|
|
53
|
+
func (h HandleLogFunc) Warnf(msg string, args ...interface{}) {
|
|
54
|
+
h(LevelWarning, fmt.Sprintf(msg, args...))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Infof handles the given message as representing an informational notice.
|
|
58
|
+
func (h HandleLogFunc) Infof(msg string, args ...interface{}) {
|
|
59
|
+
h(LevelInfo, fmt.Sprintf(msg, args...))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Debugf handles the given message as representing an internal-only debug message.
|
|
63
|
+
func (h HandleLogFunc) Debugf(msg string, args ...interface{}) {
|
|
64
|
+
h(LevelDebug, fmt.Sprintf(msg, args...))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Sub is a no-op and will return the receiver itself.
|
|
68
|
+
func (h HandleLogFunc) Sub(string) walog.Logger {
|
|
69
|
+
return h
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A Gateway represents a persistent process for establishing individual sessions between linked
|
|
73
|
+
// devices and WhatsApp.
|
|
74
|
+
type Gateway struct {
|
|
75
|
+
DBPath string // The filesystem path for the client database.
|
|
76
|
+
Name string // The name to display when linking devices on WhatsApp.
|
|
77
|
+
TempDir string // The directory to create temporary files under.
|
|
78
|
+
|
|
79
|
+
// Internal variables.
|
|
80
|
+
container *sqlstore.Container
|
|
81
|
+
logger walog.Logger
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// NewGateway returns a new, un-initialized Gateway. This function should always be followed by calls
|
|
85
|
+
// to [Gateway.Init], assuming a valid [Gateway.DBPath] is set.
|
|
86
|
+
func NewGateway() *Gateway {
|
|
87
|
+
return &Gateway{}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// SetLogHandler specifies the log handling function to use for all [Gateway] and [Session] operations.
|
|
91
|
+
func (w *Gateway) SetLogHandler(h HandleLogFunc) {
|
|
92
|
+
w.logger = HandleLogFunc(func(level ErrorLevel, message string) {
|
|
93
|
+
// Don't allow other Goroutines from using this thread, as this might lead to concurrent
|
|
94
|
+
// use of the GIL, which can lead to crashes.
|
|
95
|
+
runtime.LockOSThread()
|
|
96
|
+
defer runtime.UnlockOSThread()
|
|
97
|
+
|
|
98
|
+
h(level, message)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Init performs initialization procedures for the Gateway, and is expected to be run before any
|
|
103
|
+
// calls to [Gateway.Session].
|
|
104
|
+
func (w *Gateway) Init() error {
|
|
105
|
+
container, err := sqlstore.New("sqlite3", w.DBPath, w.logger)
|
|
106
|
+
if err != nil {
|
|
107
|
+
return err
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if w.Name != "" {
|
|
111
|
+
store.SetOSInfo(w.Name, [...]uint32{1, 0, 0})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if w.TempDir != "" {
|
|
115
|
+
tempDir = w.TempDir
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
w.container = container
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// NewSession returns a new [Session] for the LinkedDevice given. If the linked device does not have
|
|
123
|
+
// a valid ID, a pair operation will be required, as described in [Session.Login].
|
|
124
|
+
func (w *Gateway) NewSession(device LinkedDevice) *Session {
|
|
125
|
+
return &Session{device: device, gateway: w}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// CleanupSession will remove all invalid and obsolete references to the given device, and should be
|
|
129
|
+
// used when pairing a new device or unregistering from the Gateway.
|
|
130
|
+
func (w *Gateway) CleanupSession(device LinkedDevice) error {
|
|
131
|
+
devices, err := w.container.GetAllDevices()
|
|
132
|
+
if err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for _, d := range devices {
|
|
137
|
+
if d.ID == nil {
|
|
138
|
+
w.logger.Infof("Removing invalid device %s from database", d.ID.String())
|
|
139
|
+
_ = d.Delete()
|
|
140
|
+
} else if device.ID != "" {
|
|
141
|
+
if jid := device.JID(); d.ID.ToNonAD() == jid.ToNonAD() && *d.ID != jid {
|
|
142
|
+
w.logger.Infof("Removing obsolete device %s from database", d.ID.String())
|
|
143
|
+
_ = d.Delete()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var (
|
|
152
|
+
// The default path for storing temporary files.
|
|
153
|
+
tempDir = os.TempDir()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// CreateTempFile creates a temporary file in the Gateway-wide temporary directory (or the default,
|
|
157
|
+
// system-wide temporary directory, if no Gateway-specific value was set) and returns the absolute
|
|
158
|
+
// path for the file, or an error if none could be created.
|
|
159
|
+
func createTempFile(data []byte) (string, error) {
|
|
160
|
+
f, err := os.CreateTemp(tempDir, "slidge-whatsapp-*")
|
|
161
|
+
if err != nil {
|
|
162
|
+
return "", fmt.Errorf("failed creating temporary file: %w", err)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
defer f.Close()
|
|
166
|
+
if len(data) > 0 {
|
|
167
|
+
if n, err := f.Write(data); err != nil {
|
|
168
|
+
return "", fmt.Errorf("failed writing to temporary file: %w", err)
|
|
169
|
+
} else if n < len(data) {
|
|
170
|
+
return "", fmt.Errorf("failed writing to temporary file: incomplete write, want %d, write %d bytes", len(data), n)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return f.Name(), nil
|
|
175
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from slidge import BaseGateway, FormField, GatewayUser, global_config
|
|
6
|
+
|
|
7
|
+
from . import config
|
|
8
|
+
from .generated import whatsapp
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .session import Session
|
|
12
|
+
|
|
13
|
+
REGISTRATION_INSTRUCTIONS = (
|
|
14
|
+
"Continue and scan the resulting QR codes on your main device, or alternatively, "
|
|
15
|
+
"use the 'pair-phone' command to complete registration. More information at "
|
|
16
|
+
"https://slidge.im/slidge-whatsapp/user.html"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
WELCOME_MESSAGE = (
|
|
20
|
+
"Thank you for registering! Please scan the following QR code on your main device "
|
|
21
|
+
"or use the 'pair-phone' command to complete registration, or type 'help' to list "
|
|
22
|
+
"other available commands."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Gateway(BaseGateway):
|
|
27
|
+
COMPONENT_NAME = "WhatsApp (slidge)"
|
|
28
|
+
COMPONENT_TYPE = "whatsapp"
|
|
29
|
+
COMPONENT_AVATAR = "https://www.whatsapp.com/apple-touch-icon.png"
|
|
30
|
+
|
|
31
|
+
REGISTRATION_INSTRUCTIONS = REGISTRATION_INSTRUCTIONS
|
|
32
|
+
WELCOME_MESSAGE = WELCOME_MESSAGE
|
|
33
|
+
REGISTRATION_FIELDS = []
|
|
34
|
+
|
|
35
|
+
SEARCH_FIELDS = [
|
|
36
|
+
FormField(var="phone", label="Phone number", required=True),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
ROSTER_GROUP = "WhatsApp"
|
|
40
|
+
|
|
41
|
+
MARK_ALL_MESSAGES = True
|
|
42
|
+
GROUPS = True
|
|
43
|
+
PROPER_RECEIPTS = True
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
super().__init__()
|
|
47
|
+
assert config.DB_PATH is not None
|
|
48
|
+
Path(config.DB_PATH.parent).mkdir(exist_ok=True)
|
|
49
|
+
(global_config.HOME_DIR / "tmp").mkdir(exist_ok=True)
|
|
50
|
+
self.whatsapp = whatsapp.NewGateway()
|
|
51
|
+
self.whatsapp.SetLogHandler(handle_log)
|
|
52
|
+
self.whatsapp.DBPath = str(config.DB_PATH)
|
|
53
|
+
self.whatsapp.Name = "Slidge on " + str(global_config.JID)
|
|
54
|
+
self.whatsapp.TempDir = str(global_config.HOME_DIR / "tmp")
|
|
55
|
+
self.whatsapp.Init()
|
|
56
|
+
|
|
57
|
+
async def validate(self, user_jid, registration_form):
|
|
58
|
+
"""
|
|
59
|
+
Validate registration form. A no-op for WhatsApp, as actual registration takes place
|
|
60
|
+
after in-band registration commands complete.
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
async def unregister(self, user: GatewayUser):
|
|
65
|
+
"""
|
|
66
|
+
Logout from the active WhatsApp session. This will also force a remote log-out, and thus
|
|
67
|
+
require pairing on next login. For simply disconnecting the active session, look at the
|
|
68
|
+
:meth:`.Session.disconnect` function.
|
|
69
|
+
"""
|
|
70
|
+
session: "Session" = self.get_session_from_user(user) # type:ignore
|
|
71
|
+
session.whatsapp.Logout()
|
|
72
|
+
try:
|
|
73
|
+
device = whatsapp.LinkedDevice(
|
|
74
|
+
ID=session.user.legacy_module_data["device_id"]
|
|
75
|
+
)
|
|
76
|
+
self.whatsapp.CleanupSession(device)
|
|
77
|
+
except KeyError:
|
|
78
|
+
pass
|
|
79
|
+
except RuntimeError as err:
|
|
80
|
+
log.error("Failed to clean up WhatsApp session: %s", err)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_log(level, msg: str):
|
|
84
|
+
"""
|
|
85
|
+
Log given message of specified level in system-wide logger.
|
|
86
|
+
"""
|
|
87
|
+
if level == whatsapp.LevelError:
|
|
88
|
+
log.error(msg)
|
|
89
|
+
elif level == whatsapp.LevelWarning:
|
|
90
|
+
log.warning(msg)
|
|
91
|
+
elif level == whatsapp.LevelDebug:
|
|
92
|
+
log.debug(msg)
|
|
93
|
+
else:
|
|
94
|
+
log.info(msg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
log = getLogger(__name__)
|
|
File without changes
|
|
Binary file
|