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.

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