diff --git a/libmxclient/API.md b/libmxclient/API.md new file mode 100644 index 0000000..85cec8f --- /dev/null +++ b/libmxclient/API.md @@ -0,0 +1,26 @@ +# libmxclient api doc + +C API + +simple highlevel matrix api intended for bindings to script languages + +`everything is a string`™ + +this should make bindings easier. + +parameters are strings, either a simple name or json strings + +return values are either a strings starting with `ERR:` or a json result + +some funktions might return `SUCCESS.` as equivalent for `ret 0` + +returned strings must be free'ed y the caller + +it's the callers task to keep passed callback pointers alive + +## versions + +`v0` unstable, devolopment version + +all other version are stable, may get additions and security fixes, no other changes + diff --git a/libmxclient/go.mod b/libmxclient/go.mod index f4ea5da..e60c812 100644 --- a/libmxclient/go.mod +++ b/libmxclient/go.mod @@ -5,8 +5,8 @@ go 1.25.0 require ( github.com/mattn/go-sqlite3 v1.14.42 github.com/rs/zerolog v1.35.0 - go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a - maunium.net/go/mautrix v0.26.5-0.20260415212519-aa49a44fe395 + go.mau.fi/util v0.9.8 + maunium.net/go/mautrix v0.27.0 ) require ( diff --git a/libmxclient/go.sum b/libmxclient/go.sum index 222eb2d..e95280c 100644 --- a/libmxclient/go.sum +++ b/libmxclient/go.sum @@ -60,6 +60,8 @@ go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg= go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE= go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a h1:OQQF3rTJH10l6+dcP0OKnYbNDMBTGoIZZINNJm8QBG8= go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE= +go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8= +go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= @@ -117,3 +119,5 @@ maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470 h1:vehV8Ev2TzpV5DH9 maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4= maunium.net/go/mautrix v0.26.5-0.20260415212519-aa49a44fe395 h1:tJX3I0CSxuZAaAeF4VRiurfsYow8N2qOgr7k5wNbtVo= maunium.net/go/mautrix v0.26.5-0.20260415212519-aa49a44fe395/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4= +maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM= +maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs= diff --git a/libmxclient/mxclient/client.go b/libmxclient/mxclient/client.go index b8f2576..0a05680 100644 --- a/libmxclient/mxclient/client.go +++ b/libmxclient/mxclient/client.go @@ -302,6 +302,7 @@ func NewMXClient(homeserverURL string, userID id.UserID, accessToken string) (*M syncer.OnEventType(event.EventMessage, mxclient._onMessage) syncer.OnEventType(event.StateMember, mxclient._onEventMember) syncer.OnEventType(event.AccountDataDirectChats, mxclient._onAccountDataDM) + syncer.OnEventType(event.EventRedaction, mxclient._onMessage) mxclient._loadDirectMap() diff --git a/libmxclient/mxclientlib.go b/libmxclient/mxclientlib.go index 5276191..aa4b290 100644 --- a/libmxclient/mxclientlib.go +++ b/libmxclient/mxclientlib.go @@ -13,6 +13,7 @@ import ( "unsafe" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -37,8 +38,60 @@ static inline void call_c_on_sys_handler(on_message_handler_ptr ptr, char* jsonS */ import "C" +/* +error types +*/ + var apiCanceled = errors.New("canceled by api call") +/* +type conversion helpers with some basic validation +*/ + +func c2RoomID(roomid *C.char) (id.RoomID, error) { + rid := C.GoString(roomid) + if rid == "" || rid[0] != '!' { + return "", errors.New("invalid room id") + } + return id.RoomID(rid), nil +} + +func c2EventID(eventid *C.char) (id.EventID, error) { + eid := C.GoString(eventid) + if eid == "" || eid[0] != '$' { + return "", errors.New("invalid event id") + } + return id.EventID(eid), nil +} + +func c2EventType(eventtype *C.char) (et event.Type, err error) { + err = et.UnmarshalText([]byte(C.GoString(eventtype))) + return +} + +func c2ContentJSON(contentjson *C.char) (contentJSON any, err error) { + err = json.Unmarshal([]byte(C.GoString(contentjson)), &contentJSON) + return +} + +func returnJSON(out any, err error) *C.char { + if err != nil { + return C.CString(fmt.Sprintf("ERR: %v", err)) + } + res, err := json.Marshal(out) + if err != nil { + return C.CString(fmt.Sprintf("ERR: %v", err)) + } + return C.CString(string(res)) +} + +func returnErr(err error) *C.char { + if err == nil { + return C.CString("SUCCESS.") + } + return C.CString(fmt.Sprintf("ERR: %v", err)) +} + /* matrix client with c callback */ @@ -86,7 +139,7 @@ func (cli *CBClient) Set_on_sys_handler(fn C.on_sys_handler_ptr, pobj unsafe.Poi cli.on_sys_handler_pobj = pobj } -// NewCClient creates a new Matrix Client ready for syncing +// NewCBClient creates a new Matrix Client ready for syncing func NewCBClient(homeserverURL string, userID id.UserID, accessToken string) (*CBClient, error) { client, err := mxclient.NewMXClient(homeserverURL, userID, accessToken) if err != nil { @@ -355,35 +408,188 @@ func apiv0_sendmessage(cid C.int, data *C.char) *C.char { return C.CString(s) } +//export apiv0_sendmessageevent +func apiv0_sendmessageevent(cid C.int, roomid *C.char, eventtype *C.char, contentjson *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + eventType, err := c2EventType(eventtype) + if err != nil { + return returnErr(err) + } + contentJSON, err := c2ContentJSON(contentjson) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + return returnJSON(cli.SendMessageEvent(context.Background(), roomID, eventType, contentJSON)) +} + +//export apiv0_sendstateevent +func apiv0_sendstateevent(cid C.int, roomid *C.char, eventtype *C.char, statekey *C.char, contentjson *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + eventType, err := c2EventType(eventtype) + if err != nil { + return returnErr(err) + } + contentJSON, err := c2ContentJSON(contentjson) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + + return returnJSON(cli.SendStateEvent(context.Background(), roomID, eventType, C.GoString(statekey), contentJSON)) +} + +//export apiv0_stateevent +func apiv0_stateevent(cid C.int, roomid *C.char, eventtype *C.char, statekey *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + eventType, err := c2EventType(eventtype) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + + var outContent any + err = cli.StateEvent(context.Background(), roomID, eventType, C.GoString(statekey), outContent) + return returnJSON(outContent, err) +} + +//export apiv0_getaccountdata +func apiv0_getaccountdata(cid C.int, name *C.char) *C.char { + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + var outContent any + err = cli.GetAccountData(context.Background(), C.GoString(name), &outContent) + return returnJSON(outContent, err) +} + +//export apiv0_setaccountdata +func apiv0_setaccountdata(cid C.int, name *C.char, data *C.char) *C.char { + contentJSON, err := c2ContentJSON(data) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + return returnErr(cli.SetAccountData(context.Background(), C.GoString(name), contentJSON)) +} + +//export apiv0_getroomaccountdata +func apiv0_getroomaccountdata(cid C.int, roomid *C.char, name *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + var outContent any + err = cli.GetRoomAccountData(context.Background(), roomID, C.GoString(name), &outContent) + return returnJSON(outContent, err) +} + +//export apiv0_setroomaccountdata +func apiv0_setroomaccountdata(cid C.int, roomid *C.char, name *C.char, data *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + contentJSON, err := c2ContentJSON(data) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + return returnErr(cli.SetRoomAccountData(context.Background(), roomID, C.GoString(name), contentJSON)) +} + +//export apiv0_redactevent +func apiv0_redactevent(cid C.int, roomid *C.char, eventid *C.char, reason *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + eventID, err := c2EventID(eventid) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + var req mautrix.ReqRedact + req.Reason = C.GoString(reason) + resp, err := cli.RedactEvent(context.Background(), roomID, eventID, req) + return returnJSON(resp, err) +} + +//export apiv0_getevent +func apiv0_getevent(cid C.int, roomid *C.char, eventid *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } + eventID, err := c2EventID(eventid) + if err != nil { + return returnErr(err) + } + cli, err := getClient(int(cid)) + if err != nil { + return returnErr(err) + } + resp, err := cli.GetEvent(context.Background(), roomID, eventID) + return returnJSON(resp, err) +} + //export apiv0_leaveroom func apiv0_leaveroom(cid C.int, roomid *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } cli, err := getClient(int(cid)) if err != nil { return C.CString(fmt.Sprintf("ERR: %v", err)) } - err = cli.LeaveRoomAndForget(context.Background(), id.RoomID(C.GoString(roomid))) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - return C.CString("SUCCESS.") + return returnErr(cli.LeaveRoomAndForget(context.Background(), roomID)) } //export apiv0_joinroom func apiv0_joinroom(cid C.int, roomid *C.char) *C.char { + roomID, err := c2RoomID(roomid) + if err != nil { + return returnErr(err) + } cli, err := getClient(int(cid)) if err != nil { return C.CString(fmt.Sprintf("ERR: %v", err)) } - resp, err := cli.JoinRoomByID(context.Background(), id.RoomID(C.GoString(roomid))) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - out, err := json.Marshal(resp) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - s := string(out) - return C.CString(s) + resp, err := cli.JoinRoomByID(context.Background(), roomID) + return returnJSON(resp, err) } //export apiv0_joinedrooms @@ -407,12 +613,7 @@ func apiv0_joinedrooms(cid C.int) *C.char { for _, room := range resp.JoinedRooms { roomList = append(roomList, roomListItem{RoomId: room, IsDirect: cli.IsDirectRoom(room)}) } - out, err := json.Marshal(roomList) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - s := string(out) - return C.CString(s) + return returnJSON(roomList, nil) } //export apiv0_genericrequest @@ -429,10 +630,7 @@ func apiv0_genericrequest(cid C.int, method *C.char, path *C.char, data *C.char) urlPath := cli.BuildURLWithFullQuery(mautrix.BaseURLPath(bup), nil) d := C.GoString(data) resp, err := cli.MakeFullRequest(context.Background(), mautrix.FullRequest{Method: C.GoString(method), URL: urlPath, RequestBytes: []byte(d), ResponseJSON: nil}) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - return C.CString(string(resp)) + return returnJSON(resp, err) } //export apiv0_createroom @@ -500,12 +698,7 @@ func apiv0_getuserdm(cid C.int, userid *C.char) *C.char { return C.CString(fmt.Sprintf("ERR: %v", err)) } list := cli.GetUserDM(C.GoString(userid)) - out, err := json.Marshal(list) - if err != nil { - return C.CString(fmt.Sprintf("ERR: %v", err)) - } - s := string(out) - return C.CString(s) + return returnJSON(list, nil) } //export apiv0_removeclient diff --git a/pygomx/build_ffi.py b/pygomx/build_ffi.py index 3e981f8..6889ea1 100644 --- a/pygomx/build_ffi.py +++ b/pygomx/build_ffi.py @@ -60,6 +60,15 @@ ffibuilder.cdef( extern char* apiv0_startclient(int cid); extern char* apiv0_stopclient(int cid); extern char* apiv0_sendmessage(int cid, char* data); + extern char* apiv0_sendmessageevent(int cid, char* roomid, char* eventtype, char* contentjson); + extern char* apiv0_sendstateevent(int cid, char* roomid, char* eventtype, char* statekey, char* contentjson); + extern char* apiv0_stateevent(int cid, char* roomid, char* eventtype, char* statekey); + extern char* apiv0_getaccountdata(int cid, char* name); + extern char* apiv0_setaccountdata(int cid, char* name, char* data); + extern char* apiv0_getroomaccountdata(int cid, char* roomid, char* name); + extern char* apiv0_setroomaccountdata(int cid, char* roomid, char* name, char* data); + extern char* apiv0_redactevent(int cid, char* roomid, char* eventid, char* reason); + extern char* apiv0_getevent(int cid, char* roomid, char* eventid); extern char* apiv0_leaveroom(int cid, char* roomid); extern char* apiv0_joinedrooms(int cid); extern char* apiv0_joinroom(int cid, char* roomid); diff --git a/pygomx/libmxclient.def b/pygomx/libmxclient.def index 43cbb50..54469e8 100644 --- a/pygomx/libmxclient.def +++ b/pygomx/libmxclient.def @@ -17,6 +17,15 @@ apiv0_listclients apiv0_login apiv0_removeclient apiv0_sendmessage +apiv0_sendmessageevent +apiv0_sendstateevent +apiv0_stateevent +apiv0_getaccountdata +apiv0_setaccountdata +apiv0_getroomaccountdata +apiv0_setroomaccountdata +apiv0_redactevent +apiv0_getevent apiv0_set_on_event_handler apiv0_set_on_message_handler apiv0_set_on_sys_handler diff --git a/pygomx/src/pygomx/apiv0.py b/pygomx/src/pygomx/apiv0.py index 514e779..d255e3b 100644 --- a/pygomx/src/pygomx/apiv0.py +++ b/pygomx/src/pygomx/apiv0.py @@ -57,6 +57,72 @@ class ApiV0Api: ) ) + @staticmethod + def room_send_message(cid, roomid, eventtype, content): + return _stringresult( + lib.apiv0_sendmessageevent( + cid, _autostring(roomid), _autostring(eventtype), _autodict(content) + ) + ) + + @staticmethod + def room_send_state(cid, roomid, eventtype, statekey, content): + return _stringresult( + lib.apiv0_sendstateevent( + cid, + _autostring(roomid), + _autostring(eventtype), + _autostring(statekey), + _autodict(content), + ) + ) + + @staticmethod + def room_get_state(cid, roomid, eventtype, statekey): + return _stringresult( + lib.apiv0_stateevent( + cid, + _autostring(roomid), + _autostring(eventtype), + _autostring(statekey), + ) + ) + + @staticmethod + def redact_event(cid, roomid, eventid, reason): + return _stringresult( + lib.apiv0_redactevent( + cid, + _autostring(roomid), + _autostring(eventid), + _autostring(reason), + ) + ) + + @staticmethod + def account_getdata(cid, name): + return _stringresult(lib.apiv0_getaccountdata(cid, _autostring(name))) + + @staticmethod + def account_setdata(cid, name, data): + return _stringresult( + lib.apiv0_setaccountdata(cid, _autostring(name), _autodict(data)) + ) + + @staticmethod + def room_get_accountdata(cid, roomid, name): + return _stringresult( + lib.apiv0_getroomaccountdata(cid, _autostring(roomid), _autostring(name)) + ) + + @staticmethod + def room_set_accountdata(cid, roomid, name, data): + return _stringresult( + lib.apiv0_setroomaccountdata( + cid, _autostring(roomid), _autostring(name), _autodict(data) + ) + ) + @staticmethod def createdm(cid, uid): return _stringresult(lib.apiv0_createdm(cid, _autostring(uid))) @@ -69,6 +135,12 @@ class ApiV0Api: def joinroom(cid, roomid): return _stringresult(lib.apiv0_joinroom(cid, _autostring(roomid))) + @staticmethod + def getevent(cid, roomid, eventid): + return _stringresult( + lib.apiv0_getevent(cid, _autostring(roomid), _autostring(eventid)) + ) + class ApiV0: """ApiV0""" diff --git a/pygomx/src/pygomx/client.py b/pygomx/src/pygomx/client.py index c80395b..4ee4e60 100644 --- a/pygomx/src/pygomx/client.py +++ b/pygomx/src/pygomx/client.py @@ -100,6 +100,44 @@ class _AsyncClient: r = ApiV0Api.generic(self.client_id, method, path, data) return CheckApiErrorOnly(r) + async def room_send_message(self, roomid, eventtype, content): + r = ApiV0Api.room_send_message(self.client_id, roomid, eventtype, content) + return CheckApiResult(r) + + async def room_send_state(self, roomid, eventtype, statekey, content): + r = ApiV0Api.room_send_state( + self.client_id, roomid, eventtype, statekey, content + ) + return CheckApiResult(r) + + async def room_get_state(self, roomid, eventtype, statekey): + r = ApiV0Api.room_get_state(self.client_id, roomid, eventtype, statekey) + return CheckApiResult(r) + + async def account_get_data(self, name): + r = ApiV0Api.account_getdata(self.client_id, name) + return CheckApiResult(r) + + async def account_set_data(self, name, data): + r = ApiV0Api.account_setdata(self.client_id, name, data) + return CheckApiError(r) + + async def room_get_accountdata(self, roomid, name): + r = ApiV0Api.room_get_accountdata(self.client_id, roomid, name) + return CheckApiResult(r) + + async def room_set_accountdata(self, roomid, name, data): + r = ApiV0Api.room_set_accountdata(self.client_id, roomid, name, data) + return CheckApiError(r) + + async def redact_event(self, roomid, eventid, reason): + r = ApiV0Api.redact_event(self.client_id, roomid, eventid, reason) + return CheckApiResult(r) + + async def getevent(self, roomid, eventid): + r = ApiV0Api.getevent(self.client_id, roomid, eventid) + return CheckApiResult(r) + async def createdm(self, uid): r = ApiV0Api.createdm(self.client_id, uid) return CheckApiResult(r)