Add basic support for incoming reactions

This commit is contained in:
Tulir Asokan 2023-07-13 00:44:57 +03:00
parent e1603932aa
commit 376f908a03
12 changed files with 404 additions and 234 deletions

View file

@ -32,6 +32,7 @@ type Database struct {
Portal *PortalQuery
Puppet *PuppetQuery
Message *MessageQuery
Reaction *ReactionQuery
}
func New(baseDB *dbutil.Database) *Database {
@ -41,6 +42,7 @@ func New(baseDB *dbutil.Database) *Database {
db.Portal = &PortalQuery{db: db}
db.Puppet = &PuppetQuery{db: db}
db.Message = &MessageQuery{db: db}
db.Reaction = &ReactionQuery{db: db}
return db
}

View file

@ -24,11 +24,10 @@ import (
"strings"
"time"
log "maunium.net/go/maulogger/v2"
"go.mau.fi/mautrix-gmessages/libgm/binary"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/mautrix-gmessages/libgm/binary"
)
type MessageQuery struct {
@ -79,7 +78,6 @@ type MessageStatus struct {
type Message struct {
db *Database
log log.Logger
Chat Key
ID string

127
database/reaction.go Normal file
View file

@ -0,0 +1,127 @@
// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
// Copyright (C) 2023 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 database
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
)
type ReactionQuery struct {
db *Database
}
func (rq *ReactionQuery) New() *Reaction {
return &Reaction{
db: rq.db,
}
}
func (rq *ReactionQuery) getDB() *Database {
return rq.db
}
const (
getReactionByIDQuery = `
SELECT conv_id, conv_receiver, msg_id, sender, reaction, mxid FROM reaction
WHERE conv_id=$1 AND conv_receiver=$2 AND msg_id=$3 AND sender=$4
`
getReactionByMXIDQuery = `
SELECT conv_id, conv_receiver, msg_id, sender, reaction, mxid FROM reaction
WHERE mxid=$1
`
getReactionsByMessageIDQuery = `
SELECT conv_id, conv_receiver, msg_id, sender, reaction, mxid FROM reaction
WHERE conv_id=$1 AND conv_receiver=$2 AND msg_id=$3
`
insertReaction = `
INSERT INTO reaction (conv_id, conv_receiver, msg_id, sender, reaction, mxid)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (conv_id, conv_receiver, msg_id, sender)
DO UPDATE SET reaction=excluded.reaction, mxid=excluded.mxid
`
)
func (rq *ReactionQuery) GetByID(ctx context.Context, chat Key, messageID, sender string) (*Reaction, error) {
return get[*Reaction](rq, ctx, getReactionByIDQuery, chat.ID, chat.Receiver, messageID, sender)
}
func (rq *ReactionQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Reaction, error) {
return get[*Reaction](rq, ctx, getReactionByMXIDQuery, mxid)
}
func (rq *ReactionQuery) GetAllByMessage(ctx context.Context, chat Key, messageID string) ([]*Reaction, error) {
return getAll[*Reaction](rq, ctx, getReactionsByMessageIDQuery, chat.ID, chat.Receiver, messageID)
}
type Reaction struct {
db *Database
Chat Key
MessageID string
Sender string
Reaction string
MXID id.EventID
}
func (r *Reaction) Scan(row dbutil.Scannable) (*Reaction, error) {
err := row.Scan(&r.Chat.ID, &r.Chat.Receiver, &r.MessageID, &r.Sender, &r.Reaction, &r.MXID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
return nil, err
}
return r, nil
}
func (r *Reaction) Insert(ctx context.Context) error {
_, err := r.db.Conn(ctx).ExecContext(ctx, insertReaction, r.Chat.ID, r.Chat.Receiver, r.MessageID, r.Sender, r.Reaction, r.MXID)
return err
}
func (rq *ReactionQuery) MassInsert(ctx context.Context, reactions []*Reaction) error {
valueStringFormat := "($1, $2, $%d, $%d, $%d, $%d)"
if rq.db.Dialect == dbutil.SQLite {
valueStringFormat = strings.ReplaceAll(valueStringFormat, "$", "?")
}
placeholders := make([]string, len(reactions))
params := make([]any, 2+len(reactions)*4)
params[0] = reactions[0].Chat.ID
params[1] = reactions[0].Chat.Receiver
for i, msg := range reactions {
baseIndex := 2 + i*4
params[baseIndex] = msg.MessageID
params[baseIndex+1] = msg.Sender
params[baseIndex+2] = msg.Reaction
params[baseIndex+3] = msg.MXID
placeholders[i] = fmt.Sprintf(valueStringFormat, baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4)
}
query := strings.Replace(insertReaction, "($1, $2, $3, $4, $5, $6)", strings.Join(placeholders, ","), 1)
_, err := rq.db.Conn(ctx).ExecContext(ctx, query, params...)
return err
}
func (r *Reaction) Delete(ctx context.Context) error {
_, err := r.db.Conn(ctx).ExecContext(ctx, "DELETE FROM reaction WHERE conv_id=$1 AND conv_receiver=$2 AND msg_id=$3 AND sender=$4", r.Chat.ID, r.Chat.Receiver, r.MessageID, r.Sender)
return err
}

View file

@ -55,11 +55,25 @@ CREATE TABLE message (
conv_id TEXT NOT NULL,
conv_receiver BIGINT NOT NULL,
id TEXT NOT NULL,
mxid TEXT NOT NULL UNIQUE,
mxid TEXT NOT NULL,
sender TEXT NOT NULL,
timestamp BIGINT NOT NULL,
status jsonb NOT NULL,
PRIMARY KEY (conv_id, conv_receiver, id),
CONSTRAINT message_portal_fkey FOREIGN KEY (conv_id, conv_receiver) REFERENCES portal(id, receiver) ON DELETE CASCADE
CONSTRAINT message_portal_fkey FOREIGN KEY (conv_id, conv_receiver) REFERENCES portal(id, receiver) ON DELETE CASCADE,
CONSTRAINT message_mxid_unique UNIQUE (mxid)
);
CREATE TABLE reaction (
conv_id TEXT NOT NULL,
conv_receiver BIGINT NOT NULL,
msg_id TEXT NOT NULL,
sender TEXT NOT NULL,
reaction TEXT NOT NULL,
mxid TEXT NOT NULL,
PRIMARY KEY (conv_id, conv_receiver, msg_id, sender),
CONSTRAINT reaction_message_fkey FOREIGN KEY (conv_id, conv_receiver, msg_id) REFERENCES message(conv_id, conv_receiver, id) ON DELETE CASCADE,
CONSTRAINT reaction_mxid_unique UNIQUE (mxid)
)

View file

@ -21,3 +21,61 @@ func DecodeProtoMessage(data []byte, message proto.Message) error {
}
return nil
}
func (et EmojiType) Unicode() string {
switch et {
case EmojiType_LIKE:
return "👍"
case EmojiType_LOVE:
return "😍"
case EmojiType_LAUGH:
return "😂"
case EmojiType_SURPRISED:
return "😮"
case EmojiType_SAD:
return "😥"
case EmojiType_ANGRY:
return "😠"
case EmojiType_DISLIKE:
return "👎"
case EmojiType_QUESTIONING:
return "🤔"
case EmojiType_CRYING_FACE:
return "😢"
case EmojiType_POUTING_FACE:
return "😡"
case EmojiType_RED_HEART:
return "❤️"
default:
return ""
}
}
func UnicodeToEmojiType(emoji string) EmojiType {
switch emoji {
case "👍":
return EmojiType_LIKE
case "😍":
return EmojiType_LOVE
case "😂":
return EmojiType_LAUGH
case "😮":
return EmojiType_SURPRISED
case "😥":
return EmojiType_SAD
case "😠":
return EmojiType_ANGRY
case "👎":
return EmojiType_DISLIKE
case "🤔":
return EmojiType_QUESTIONING
case "😢":
return EmojiType_CRYING_FACE
case "😡":
return EmojiType_POUTING_FACE
case "❤", "❤️":
return EmojiType_RED_HEART
default:
return EmojiType_CUSTOM
}
}

View file

@ -21,13 +21,13 @@ message SendReactionResponse {
}
message ReactionData {
bytes emojiUnicode = 1;
int64 emojiType = 2;
string unicode = 1;
EmojiType type = 2;
}
message ReactionResponse {
ReactionData data = 1;
repeated string reactorParticipantsID = 2; // participants reacted with this emoji
repeated string participantIDs = 2;
}
message EmojiMeta {
@ -35,11 +35,11 @@ message EmojiMeta {
}
message EmojiMetaData {
bytes emojiUnicode = 1;
string unicode = 1;
repeated string names = 2;
}
enum Emojis {
enum EmojiType {
REACTION_TYPE_UNSPECIFIED = 0;
LIKE = 1;
LOVE = 2;

View file

@ -72,27 +72,27 @@ func (Reaction) EnumDescriptor() ([]byte, []int) {
return file_reactions_proto_rawDescGZIP(), []int{0}
}
type Emojis int32
type EmojiType int32
const (
Emojis_REACTION_TYPE_UNSPECIFIED Emojis = 0
Emojis_LIKE Emojis = 1
Emojis_LOVE Emojis = 2
Emojis_LAUGH Emojis = 3
Emojis_SURPRISED Emojis = 4
Emojis_SAD Emojis = 5
Emojis_ANGRY Emojis = 6
Emojis_DISLIKE Emojis = 7
Emojis_CUSTOM Emojis = 8
Emojis_QUESTIONING Emojis = 9
Emojis_CRYING_FACE Emojis = 10
Emojis_POUTING_FACE Emojis = 11
Emojis_RED_HEART Emojis = 12
EmojiType_REACTION_TYPE_UNSPECIFIED EmojiType = 0
EmojiType_LIKE EmojiType = 1
EmojiType_LOVE EmojiType = 2
EmojiType_LAUGH EmojiType = 3
EmojiType_SURPRISED EmojiType = 4
EmojiType_SAD EmojiType = 5
EmojiType_ANGRY EmojiType = 6
EmojiType_DISLIKE EmojiType = 7
EmojiType_CUSTOM EmojiType = 8
EmojiType_QUESTIONING EmojiType = 9
EmojiType_CRYING_FACE EmojiType = 10
EmojiType_POUTING_FACE EmojiType = 11
EmojiType_RED_HEART EmojiType = 12
)
// Enum value maps for Emojis.
// Enum value maps for EmojiType.
var (
Emojis_name = map[int32]string{
EmojiType_name = map[int32]string{
0: "REACTION_TYPE_UNSPECIFIED",
1: "LIKE",
2: "LOVE",
@ -107,7 +107,7 @@ var (
11: "POUTING_FACE",
12: "RED_HEART",
}
Emojis_value = map[string]int32{
EmojiType_value = map[string]int32{
"REACTION_TYPE_UNSPECIFIED": 0,
"LIKE": 1,
"LOVE": 2,
@ -124,30 +124,30 @@ var (
}
)
func (x Emojis) Enum() *Emojis {
p := new(Emojis)
func (x EmojiType) Enum() *EmojiType {
p := new(EmojiType)
*p = x
return p
}
func (x Emojis) String() string {
func (x EmojiType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Emojis) Descriptor() protoreflect.EnumDescriptor {
func (EmojiType) Descriptor() protoreflect.EnumDescriptor {
return file_reactions_proto_enumTypes[1].Descriptor()
}
func (Emojis) Type() protoreflect.EnumType {
func (EmojiType) Type() protoreflect.EnumType {
return &file_reactions_proto_enumTypes[1]
}
func (x Emojis) Number() protoreflect.EnumNumber {
func (x EmojiType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Emojis.Descriptor instead.
func (Emojis) EnumDescriptor() ([]byte, []int) {
// Deprecated: Use EmojiType.Descriptor instead.
func (EmojiType) EnumDescriptor() ([]byte, []int) {
return file_reactions_proto_rawDescGZIP(), []int{1}
}
@ -266,8 +266,8 @@ type ReactionData struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
EmojiUnicode []byte `protobuf:"bytes,1,opt,name=emojiUnicode,proto3" json:"emojiUnicode,omitempty"`
EmojiType int64 `protobuf:"varint,2,opt,name=emojiType,proto3" json:"emojiType,omitempty"`
Unicode string `protobuf:"bytes,1,opt,name=unicode,proto3" json:"unicode,omitempty"`
Type EmojiType `protobuf:"varint,2,opt,name=type,proto3,enum=reactions.EmojiType" json:"type,omitempty"`
}
func (x *ReactionData) Reset() {
@ -302,18 +302,18 @@ func (*ReactionData) Descriptor() ([]byte, []int) {
return file_reactions_proto_rawDescGZIP(), []int{2}
}
func (x *ReactionData) GetEmojiUnicode() []byte {
func (x *ReactionData) GetUnicode() string {
if x != nil {
return x.EmojiUnicode
return x.Unicode
}
return nil
return ""
}
func (x *ReactionData) GetEmojiType() int64 {
func (x *ReactionData) GetType() EmojiType {
if x != nil {
return x.EmojiType
return x.Type
}
return 0
return EmojiType_REACTION_TYPE_UNSPECIFIED
}
type ReactionResponse struct {
@ -322,7 +322,7 @@ type ReactionResponse struct {
unknownFields protoimpl.UnknownFields
Data *ReactionData `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
ReactorParticipantsID []string `protobuf:"bytes,2,rep,name=reactorParticipantsID,proto3" json:"reactorParticipantsID,omitempty"` // participants reacted with this emoji
ParticipantIDs []string `protobuf:"bytes,2,rep,name=participantIDs,proto3" json:"participantIDs,omitempty"`
}
func (x *ReactionResponse) Reset() {
@ -364,9 +364,9 @@ func (x *ReactionResponse) GetData() *ReactionData {
return nil
}
func (x *ReactionResponse) GetReactorParticipantsID() []string {
func (x *ReactionResponse) GetParticipantIDs() []string {
if x != nil {
return x.ReactorParticipantsID
return x.ParticipantIDs
}
return nil
}
@ -423,7 +423,7 @@ type EmojiMetaData struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
EmojiUnicode []byte `protobuf:"bytes,1,opt,name=emojiUnicode,proto3" json:"emojiUnicode,omitempty"`
Unicode string `protobuf:"bytes,1,opt,name=unicode,proto3" json:"unicode,omitempty"`
Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"`
}
@ -459,11 +459,11 @@ func (*EmojiMetaData) Descriptor() ([]byte, []int) {
return file_reactions_proto_rawDescGZIP(), []int{5}
}
func (x *EmojiMetaData) GetEmojiUnicode() []byte {
func (x *EmojiMetaData) GetUnicode() string {
if x != nil {
return x.EmojiUnicode
return x.Unicode
}
return nil
return ""
}
func (x *EmojiMetaData) GetNames() []string {
@ -490,47 +490,46 @@ var file_reactions_proto_rawDesc = []byte{
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x30, 0x0a, 0x14,
0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x50,
0x0a, 0x0c, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x22,
0x0a, 0x0c, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x55, 0x6e, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x55, 0x6e, 0x69, 0x63, 0x6f,
0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x54, 0x79, 0x70, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x54, 0x79, 0x70, 0x65,
0x22, 0x75, 0x0a, 0x10, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52,
0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74,
0x61, 0x12, 0x34, 0x0a, 0x15, 0x72, 0x65, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x61, 0x72, 0x74,
0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x49, 0x44, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
0x52, 0x15, 0x72, 0x65, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69,
0x70, 0x61, 0x6e, 0x74, 0x73, 0x49, 0x44, 0x22, 0x4b, 0x0a, 0x09, 0x45, 0x6d, 0x6f, 0x6a, 0x69,
0x4d, 0x65, 0x74, 0x61, 0x12, 0x3e, 0x0a, 0x0d, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x4d, 0x65, 0x74,
0x61, 0x44, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x72, 0x65,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x45, 0x6d, 0x6f, 0x6a, 0x69, 0x4d, 0x65, 0x74,
0x61, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x4d, 0x65, 0x74, 0x61,
0x44, 0x61, 0x74, 0x61, 0x22, 0x49, 0x0a, 0x0d, 0x45, 0x6d, 0x6f, 0x6a, 0x69, 0x4d, 0x65, 0x74,
0x61, 0x44, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x6d, 0x6f, 0x6a, 0x69, 0x55, 0x6e,
0x69, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x6d, 0x6f,
0x6a, 0x69, 0x55, 0x6e, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d,
0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x2a,
0x3c, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x55,
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
0x41, 0x44, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x10,
0x02, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x57, 0x49, 0x54, 0x43, 0x48, 0x10, 0x03, 0x2a, 0xc5, 0x01,
0x0a, 0x06, 0x45, 0x6d, 0x6f, 0x6a, 0x69, 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x43,
0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x4b, 0x45, 0x10,
0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x4f, 0x56, 0x45, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x4c,
0x41, 0x55, 0x47, 0x48, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x52, 0x50, 0x52, 0x49,
0x53, 0x45, 0x44, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x41, 0x44, 0x10, 0x05, 0x12, 0x09,
0x0a, 0x05, 0x41, 0x4e, 0x47, 0x52, 0x59, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x49, 0x53,
0x4c, 0x49, 0x4b, 0x45, 0x10, 0x07, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d,
0x10, 0x08, 0x12, 0x0f, 0x0a, 0x0b, 0x51, 0x55, 0x45, 0x53, 0x54, 0x49, 0x4f, 0x4e, 0x49, 0x4e,
0x47, 0x10, 0x09, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x52, 0x59, 0x49, 0x4e, 0x47, 0x5f, 0x46, 0x41,
0x43, 0x45, 0x10, 0x0a, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f,
0x46, 0x41, 0x43, 0x45, 0x10, 0x0b, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x45, 0x44, 0x5f, 0x48, 0x45,
0x41, 0x52, 0x54, 0x10, 0x0c, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x62,
0x69, 0x6e, 0x61, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x52,
0x0a, 0x0c, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18,
0x0a, 0x07, 0x75, 0x6e, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x07, 0x75, 0x6e, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x2e, 0x45, 0x6d, 0x6f, 0x6a, 0x69, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79,
0x70, 0x65, 0x22, 0x67, 0x0a, 0x10, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x2e, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64,
0x61, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61,
0x6e, 0x74, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x72,
0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x49, 0x44, 0x73, 0x22, 0x4b, 0x0a, 0x09, 0x45,
0x6d, 0x6f, 0x6a, 0x69, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x3e, 0x0a, 0x0d, 0x65, 0x6d, 0x6f, 0x6a,
0x69, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x18, 0x2e, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x45, 0x6d, 0x6f, 0x6a,
0x69, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x6d, 0x6f, 0x6a, 0x69,
0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x22, 0x3f, 0x0a, 0x0d, 0x45, 0x6d, 0x6f, 0x6a,
0x69, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x6e, 0x69,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x6e, 0x69, 0x63,
0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x2a, 0x3c, 0x0a, 0x08, 0x52, 0x65, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x44, 0x44, 0x10, 0x01, 0x12,
0x0a, 0x0a, 0x06, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x53,
0x57, 0x49, 0x54, 0x43, 0x48, 0x10, 0x03, 0x2a, 0xc8, 0x01, 0x0a, 0x09, 0x45, 0x6d, 0x6f, 0x6a,
0x69, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x43, 0x54, 0x49, 0x4f,
0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x4b, 0x45, 0x10, 0x01, 0x12, 0x08,
0x0a, 0x04, 0x4c, 0x4f, 0x56, 0x45, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x41, 0x55, 0x47,
0x48, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x52, 0x50, 0x52, 0x49, 0x53, 0x45, 0x44,
0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x41, 0x44, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x41,
0x4e, 0x47, 0x52, 0x59, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x49, 0x53, 0x4c, 0x49, 0x4b,
0x45, 0x10, 0x07, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x08, 0x12,
0x0f, 0x0a, 0x0b, 0x51, 0x55, 0x45, 0x53, 0x54, 0x49, 0x4f, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x09,
0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x52, 0x59, 0x49, 0x4e, 0x47, 0x5f, 0x46, 0x41, 0x43, 0x45, 0x10,
0x0a, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x46, 0x41, 0x43,
0x45, 0x10, 0x0b, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x45, 0x44, 0x5f, 0x48, 0x45, 0x41, 0x52, 0x54,
0x10, 0x0c, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x62, 0x69, 0x6e, 0x61,
0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -549,7 +548,7 @@ var file_reactions_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_reactions_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_reactions_proto_goTypes = []interface{}{
(Reaction)(0), // 0: reactions.Reaction
(Emojis)(0), // 1: reactions.Emojis
(EmojiType)(0), // 1: reactions.EmojiType
(*SendReactionPayload)(nil), // 2: reactions.SendReactionPayload
(*SendReactionResponse)(nil), // 3: reactions.SendReactionResponse
(*ReactionData)(nil), // 4: reactions.ReactionData
@ -560,13 +559,14 @@ var file_reactions_proto_goTypes = []interface{}{
var file_reactions_proto_depIdxs = []int32{
4, // 0: reactions.SendReactionPayload.reactionData:type_name -> reactions.ReactionData
0, // 1: reactions.SendReactionPayload.action:type_name -> reactions.Reaction
4, // 2: reactions.ReactionResponse.data:type_name -> reactions.ReactionData
7, // 3: reactions.EmojiMeta.emojiMetaData:type_name -> reactions.EmojiMetaData
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
1, // 2: reactions.ReactionData.type:type_name -> reactions.EmojiType
4, // 3: reactions.ReactionResponse.data:type_name -> reactions.ReactionData
7, // 4: reactions.EmojiMeta.emojiMetaData:type_name -> reactions.EmojiMetaData
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_reactions_proto_init() }

View file

@ -10,12 +10,7 @@ type Messages struct {
client *Client
}
func (m *Messages) React(reactionBuilder *ReactionBuilder) (*binary.SendReactionResponse, error) {
payload, buildErr := reactionBuilder.Build()
if buildErr != nil {
return nil, buildErr
}
func (m *Messages) React(payload *binary.SendReactionPayload) (*binary.SendReactionResponse, error) {
actionType := binary.ActionType_SEND_REACTION
sentRequestId, sendErr := m.client.sessionHandler.completeSendMessage(actionType, true, payload)

View file

@ -1,14 +0,0 @@
package metadata
var Emojis = map[string]int64{
"\U0001F44D": 1, // 👍
"\U0001F60D": 2, // 😍
"\U0001F602": 3, // 😂
"\U0001F62E": 4, // 😮
"\U0001F625": 5, // 😥
"\U0001F622": 10, // 😢
"\U0001F620": 6, // 😠
"\U0001F621": 11, // 😡
"\U0001F44E": 7, // 👎
"\U00002764": 12, // ❤️
}

View file

@ -1,88 +0,0 @@
package libgm
import (
"fmt"
"go.mau.fi/mautrix-gmessages/libgm/binary"
"go.mau.fi/mautrix-gmessages/libgm/metadata"
)
type ReactionBuilderError struct {
errMsg string
}
func (rbe *ReactionBuilderError) Error() string {
return fmt.Sprintf("Failed to build reaction builder: %s", rbe.errMsg)
}
type ReactionBuilder struct {
messageID string
emoji []byte
action binary.Reaction
emojiType int64
}
func (rb *ReactionBuilder) SetAction(action binary.Reaction) *ReactionBuilder {
rb.action = action
return rb
}
/*
Emoji is a unicode string like "\U0001F44D" or a string like "👍"
*/
func (rb *ReactionBuilder) SetEmoji(emoji string) *ReactionBuilder {
emojiType, exists := metadata.Emojis[emoji]
if exists {
rb.emojiType = emojiType
} else {
rb.emojiType = 8
}
rb.emoji = []byte(emoji)
return rb
}
func (rb *ReactionBuilder) SetMessageID(messageId string) *ReactionBuilder {
rb.messageID = messageId
return rb
}
func (rb *ReactionBuilder) Build() (*binary.SendReactionPayload, error) {
if rb.messageID == "" {
return nil, &ReactionBuilderError{
errMsg: "messageID can not be empty",
}
}
if rb.action == 0 {
return nil, &ReactionBuilderError{
errMsg: "action can not be empty",
}
}
if rb.emojiType == 0 {
return nil, &ReactionBuilderError{
errMsg: "failed to set emojiType",
}
}
if rb.emoji == nil {
return nil, &ReactionBuilderError{
errMsg: "failed to set emoji",
}
}
return &binary.SendReactionPayload{
MessageID: rb.messageID,
ReactionData: &binary.ReactionData{
EmojiUnicode: rb.emoji,
EmojiType: rb.emojiType,
},
Action: rb.action,
}, nil
}
func (c *Client) NewReactionBuilder() *ReactionBuilder {
return &ReactionBuilder{}
}

109
portal.go
View file

@ -30,6 +30,7 @@ import (
"github.com/gabriel-vasile/mimetype"
"github.com/rs/zerolog"
"maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/util/variationselector"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
@ -358,7 +359,9 @@ func (portal *Portal) handleMessage(source *User, evt *binary.Message) {
return
}
eventTS := time.UnixMicro(evt.GetTimestamp())
if eventTS.After(portal.lastMessageTS) {
portal.lastMessageTS = eventTS
}
log := portal.zlog.With().
Str("message_id", evt.MessageID).
Str("participant_id", evt.ParticipantID).
@ -369,6 +372,9 @@ func (portal *Portal) handleMessage(source *User, evt *binary.Message) {
case binary.MessageStatusType_INCOMING_AUTO_DOWNLOADING, binary.MessageStatusType_INCOMING_RETRYING_AUTO_DOWNLOAD:
log.Debug().Msg("Not handling incoming message that is auto downloading")
return
case binary.MessageStatusType_MESSAGE_DELETED:
// TODO handle deletion
return
}
if hasInProgressMedia(evt) {
log.Debug().Msg("Not handling incoming message that doesn't have full media yet")
@ -377,15 +383,13 @@ func (portal *Portal) handleMessage(source *User, evt *binary.Message) {
if evtID := portal.isOutgoingMessage(evt); evtID != "" {
log.Debug().Str("event_id", evtID.String()).Msg("Got echo for outgoing message")
return
} else if portal.isRecentlyHandled(evt.MessageID) {
log.Debug().Msg("Not handling recent duplicate message")
return
}
existingMsg, err := portal.bridge.DB.Message.GetByID(ctx, portal.Key, evt.MessageID)
if err != nil {
log.Err(err).Msg("Failed to check if message is duplicate")
} else if existingMsg != nil {
log.Debug().Msg("Not handling duplicate message")
portal.syncReactions(ctx, source, existingMsg, evt.Reactions)
return
}
@ -408,6 +412,73 @@ func (portal *Portal) handleMessage(source *User, evt *binary.Message) {
log.Debug().Interface("event_ids", eventIDs).Msg("Handled message")
}
func (portal *Portal) syncReactions(ctx context.Context, source *User, message *database.Message, reactions []*binary.ReactionResponse) {
log := zerolog.Ctx(ctx)
existing, err := portal.bridge.DB.Reaction.GetAllByMessage(ctx, portal.Key, message.ID)
if err != nil {
log.Err(err).Msg("Failed to get existing reactions from db to sync reactions")
return
}
remove := make(map[string]*database.Reaction, len(existing))
for _, reaction := range existing {
remove[reaction.Sender] = reaction
}
for _, reaction := range reactions {
emoji := reaction.GetData().GetUnicode()
if emoji == "" {
emoji = reaction.GetData().GetType().Unicode()
if emoji == "" {
continue
}
}
for _, participant := range reaction.GetParticipantIDs() {
dbReaction, ok := remove[participant]
if ok {
delete(remove, participant)
if dbReaction.Reaction == emoji {
continue
}
}
intent := portal.getIntent(ctx, source, participant)
if intent == nil {
continue
}
var resp *mautrix.RespSendEvent
resp, err = intent.SendReaction(portal.MXID, message.MXID, variationselector.Add(emoji))
if err != nil {
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to send reaction")
continue
}
if dbReaction == nil {
dbReaction = portal.bridge.DB.Reaction.New()
dbReaction.Chat = portal.Key
dbReaction.Sender = participant
dbReaction.MessageID = message.ID
} else if _, err = intent.RedactEvent(portal.MXID, dbReaction.MXID); err != nil {
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to redact old reaction after adding new one")
}
dbReaction.Reaction = emoji
dbReaction.MXID = resp.EventID
err = dbReaction.Insert(ctx)
if err != nil {
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to insert added reaction into db")
}
}
}
for _, reaction := range remove {
intent := portal.getIntent(ctx, source, reaction.Sender)
if intent == nil {
continue
}
_, err = intent.RedactEvent(portal.MXID, reaction.MXID)
if err != nil {
log.Err(err).Str("reaction_sender_id", reaction.Sender).Msg("Failed to redact removed reaction")
} else if err = reaction.Delete(ctx); err != nil {
log.Err(err).Str("reaction_sender_id", reaction.Sender).Msg("Failed to remove reaction from db")
}
}
}
type ConvertedMessagePart struct {
Content *event.MessageEventContent
Extra map[string]any
@ -423,6 +494,24 @@ type ConvertedMessage struct {
Parts []ConvertedMessagePart
}
func (portal *Portal) getIntent(ctx context.Context, source *User, participant string) *appservice.IntentAPI {
if participant == portal.SelfUserID {
intent := source.DoublePuppetIntent
if intent == nil {
zerolog.Ctx(ctx).Debug().Msg("Dropping message from self as double puppeting is not enabled")
return nil
}
return intent
} else {
puppet := source.GetPuppetByID(participant, "")
if puppet == nil {
zerolog.Ctx(ctx).Debug().Str("participant_id", participant).Msg("Dropping message from unknown participant")
return nil
}
return puppet.IntentFor(portal)
}
}
func (portal *Portal) convertGoogleMessage(ctx context.Context, source *User, evt *binary.Message, backfill bool) *ConvertedMessage {
log := zerolog.Ctx(ctx)
@ -430,22 +519,10 @@ func (portal *Portal) convertGoogleMessage(ctx context.Context, source *User, ev
cm.SenderID = evt.ParticipantID
cm.ID = evt.MessageID
cm.Timestamp = time.UnixMicro(evt.Timestamp)
// TODO is there a fromMe flag?
if evt.GetParticipantID() == portal.SelfUserID {
cm.Intent = source.DoublePuppetIntent
cm.Intent = portal.getIntent(ctx, source, evt.ParticipantID)
if cm.Intent == nil {
log.Debug().Msg("Dropping message from self as double puppeting is not enabled")
return nil
}
} else {
puppet := source.GetPuppetByID(evt.ParticipantID, "")
if puppet == nil {
log.Debug().Str("participant_id", evt.ParticipantID).Msg("Dropping message from unknown participant")
return nil
}
cm.Intent = puppet.IntentFor(portal)
}
var replyTo id.EventID
if evt.GetReplyMessage() != nil {

View file

@ -568,6 +568,7 @@ func (user *User) syncConversation(v *binary.Conversation) {
portal := user.GetPortalByID(v.GetConversationID())
if portal.MXID != "" {
switch updateType {
// TODO also delete if blocked?
case binary.ConvUpdateTypes_DELETED:
user.zlog.Info().Str("conversation_id", portal.ID).Msg("Got delete event, cleaning up portal")
portal.Delete()