slidge-whatsapp 0.2.2__cp313-cp313-manylinux_2_36_aarch64.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,181 @@
1
+ package whatsapp
2
+
3
+ import (
4
+ // Standard library.
5
+ "fmt"
6
+ "log/slog"
7
+ "os"
8
+ "runtime"
9
+
10
+ // Internal packages.
11
+ "codeberg.org/slidge/slidge-whatsapp/slidge_whatsapp/media"
12
+
13
+ // Third-party libraries.
14
+ _ "github.com/mattn/go-sqlite3"
15
+ "go.mau.fi/whatsmeow/store"
16
+ "go.mau.fi/whatsmeow/store/sqlstore"
17
+ "go.mau.fi/whatsmeow/types"
18
+ walog "go.mau.fi/whatsmeow/util/log"
19
+ )
20
+
21
+ const (
22
+ // Maximum number of concurrent gateway calls to handle before blocking.
23
+ maxConcurrentGatewayCalls = 1024
24
+ )
25
+
26
+ // A LinkedDevice represents a unique pairing session between the gateway and WhatsApp. It is not
27
+ // unique to the underlying "main" device (or phone number), as multiple linked devices may be paired
28
+ // with any main device.
29
+ type LinkedDevice struct {
30
+ // ID is an opaque string identifying this LinkedDevice to the Session. Noted that this string
31
+ // is currently equivalent to a password, and needs to be protected accordingly.
32
+ ID string
33
+ }
34
+
35
+ // JID returns the WhatsApp JID corresponding to the LinkedDevice ID. Empty or invalid device IDs
36
+ // may return invalid JIDs, and this function does not handle errors.
37
+ func (d LinkedDevice) JID() types.JID {
38
+ jid, _ := types.ParseJID(d.ID)
39
+ return jid
40
+ }
41
+
42
+ // A Gateway represents a persistent process for establishing individual sessions between linked
43
+ // devices and WhatsApp.
44
+ type Gateway struct {
45
+ DBPath string // The filesystem path for the client database.
46
+ Name string // The name to display when linking devices on WhatsApp.
47
+ LogLevel string // The verbosity level to use when logging messages.
48
+ TempDir string // The directory to create temporary files under.
49
+
50
+ // Internal variables.
51
+ container *sqlstore.Container
52
+ callChan chan (func())
53
+ logger walog.Logger
54
+ }
55
+
56
+ // NewGateway returns a new, un-initialized Gateway. This function should always be followed by calls
57
+ // to [Gateway.Init], assuming a valid [Gateway.DBPath] is set.
58
+ func NewGateway() *Gateway {
59
+ return &Gateway{}
60
+ }
61
+
62
+ // Init performs initialization procedures for the Gateway, and is expected to be run before any
63
+ // calls to [Gateway.Session].
64
+ func (w *Gateway) Init() error {
65
+ w.logger = logger{
66
+ module: "Slidge",
67
+ logger: slog.New(
68
+ slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel(w.LogLevel)}),
69
+ ),
70
+ }
71
+
72
+ container, err := sqlstore.New("sqlite3", w.DBPath, w.logger)
73
+ if err != nil {
74
+ return err
75
+ }
76
+
77
+ if w.Name != "" {
78
+ store.SetOSInfo(w.Name, [...]uint32{1, 0, 0})
79
+ }
80
+
81
+ if w.TempDir != "" {
82
+ media.SetTempDirectory(w.TempDir)
83
+ }
84
+
85
+ w.callChan = make(chan func(), maxConcurrentGatewayCalls)
86
+ w.container = container
87
+
88
+ go func() {
89
+ // Don't allow other Goroutines from using this thread, as this might lead to concurrent use of
90
+ // the GIL, which can lead to crashes.
91
+ runtime.LockOSThread()
92
+ defer runtime.UnlockOSThread()
93
+ for fn := range w.callChan {
94
+ fn()
95
+ }
96
+ }()
97
+
98
+ return nil
99
+ }
100
+
101
+ // NewSession returns a new [Session] for the LinkedDevice given. If the linked device does not have
102
+ // a valid ID, a pair operation will be required, as described in [Session.Login].
103
+ func (w *Gateway) NewSession(device LinkedDevice) *Session {
104
+ return &Session{device: device, gateway: w}
105
+ }
106
+
107
+ // CleanupSession will remove all invalid and obsolete references to the given device, and should be
108
+ // used when pairing a new device or unregistering from the Gateway.
109
+ func (w *Gateway) CleanupSession(device LinkedDevice) error {
110
+ devices, err := w.container.GetAllDevices()
111
+ if err != nil {
112
+ return err
113
+ }
114
+
115
+ for _, d := range devices {
116
+ if d.ID == nil {
117
+ w.logger.Infof("Removing invalid device %s from database", d.ID.String())
118
+ _ = d.Delete()
119
+ } else if device.ID != "" {
120
+ if jid := device.JID(); d.ID.ToNonAD() == jid.ToNonAD() && *d.ID != jid {
121
+ w.logger.Infof("Removing obsolete device %s from database", d.ID.String())
122
+ _ = d.Delete()
123
+ }
124
+ }
125
+ }
126
+
127
+ return nil
128
+ }
129
+
130
+ // A LogLevel represents a mapping between Python standard logging levels and Go standard logging
131
+ // levels.
132
+ type logLevel string
133
+
134
+ var _ slog.Leveler = logLevel("")
135
+
136
+ // Level returns the Go equivalent logging level for the Python logging level represented.
137
+ func (l logLevel) Level() slog.Level {
138
+ switch l {
139
+ case "FATAL", "CRITICAL", "ERROR":
140
+ return slog.LevelError
141
+ case "WARN", "WARNING":
142
+ return slog.LevelWarn
143
+ case "DEBUG":
144
+ return slog.LevelDebug
145
+ default:
146
+ return slog.LevelInfo
147
+ }
148
+ }
149
+
150
+ // A Logger represents a mapping between a WhatsMeow logger and Go standard logging functions.
151
+ type logger struct {
152
+ module string
153
+ logger *slog.Logger
154
+ }
155
+
156
+ var _ walog.Logger = logger{}
157
+
158
+ // Errorf handles the given message as representing a (typically) fatal error.
159
+ func (l logger) Errorf(msg string, args ...interface{}) {
160
+ l.logger.Error(fmt.Sprintf(msg, args...))
161
+ }
162
+
163
+ // Warn handles the given message as representing a non-fatal error or warning thereof.
164
+ func (l logger) Warnf(msg string, args ...interface{}) {
165
+ l.logger.Warn(fmt.Sprintf(msg, args...))
166
+ }
167
+
168
+ // Infof handles the given message as representing an informational notice.
169
+ func (l logger) Infof(msg string, args ...interface{}) {
170
+ l.logger.Info(fmt.Sprintf(msg, args...))
171
+ }
172
+
173
+ // Debugf handles the given message as representing an internal-only debug message.
174
+ func (l logger) Debugf(msg string, args ...interface{}) {
175
+ l.logger.Debug(fmt.Sprintf(msg, args...))
176
+ }
177
+
178
+ // Sub is a no-op and will return the receiver itself.
179
+ func (l logger) Sub(module string) walog.Logger {
180
+ return logger{logger: l.logger.With(slog.String("module", l.module+"."+module))}
181
+ }
@@ -0,0 +1,82 @@
1
+ from logging import getLevelName, 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
+ ROSTER_GROUP = "WhatsApp"
31
+
32
+ REGISTRATION_INSTRUCTIONS = REGISTRATION_INSTRUCTIONS
33
+ WELCOME_MESSAGE = WELCOME_MESSAGE
34
+ REGISTRATION_FIELDS = []
35
+
36
+ SEARCH_FIELDS = [
37
+ FormField(var="phone", label="Phone number", required=True),
38
+ ]
39
+
40
+ MARK_ALL_MESSAGES = True
41
+ GROUPS = True
42
+ PROPER_RECEIPTS = True
43
+
44
+ def __init__(self):
45
+ super().__init__()
46
+ self.whatsapp = whatsapp.NewGateway()
47
+ self.whatsapp.Name = "Slidge on " + str(global_config.JID)
48
+ self.whatsapp.LogLevel = getLevelName(getLogger().level)
49
+
50
+ assert config.DB_PATH is not None
51
+ Path(config.DB_PATH.parent).mkdir(exist_ok=True)
52
+ self.whatsapp.DBPath = str(config.DB_PATH)
53
+
54
+ (global_config.HOME_DIR / "tmp").mkdir(exist_ok=True)
55
+ self.whatsapp.TempDir = str(global_config.HOME_DIR / "tmp")
56
+ self.whatsapp.Init()
57
+
58
+ async def validate(self, user_jid, registration_form):
59
+ """
60
+ Validate registration form. A no-op for WhatsApp, as actual registration takes place
61
+ after in-band registration commands complete; see :meth:`.Session.login` for more.
62
+ """
63
+ pass
64
+
65
+ async def unregister(self, user: GatewayUser):
66
+ """
67
+ Logout from the active WhatsApp session. This will also force a remote log-out, and thus
68
+ require pairing on next login. For simply disconnecting the active session, look at the
69
+ :meth:`.Session.disconnect` function.
70
+ """
71
+ session: "Session" = self.get_session_from_user(user) # type:ignore
72
+ session.whatsapp.Logout()
73
+ try:
74
+ device_id = session.user.legacy_module_data["device_id"]
75
+ self.whatsapp.CleanupSession(whatsapp.LinkedDevice(ID=device_id))
76
+ except KeyError:
77
+ pass
78
+ except RuntimeError as err:
79
+ log.error("Failed to clean up WhatsApp session: %s", err)
80
+
81
+
82
+ log = getLogger(__name__)
File without changes