feat: add chapter eight call records
This commit is contained in:
@@ -57,6 +57,29 @@ func main() {
|
|||||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
"CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci",
|
"CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci",
|
||||||
|
`CREATE TABLE IF NOT EXISTS tinode.im_call_records(
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
call_id VARCHAR(64) NOT NULL,
|
||||||
|
topic CHAR(25) NOT NULL,
|
||||||
|
seq_id INT NOT NULL,
|
||||||
|
caller_id BIGINT NOT NULL,
|
||||||
|
callee_id BIGINT NOT NULL,
|
||||||
|
call_type VARCHAR(16) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
end_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
started_at DATETIME(3) NOT NULL,
|
||||||
|
accepted_at DATETIME(3),
|
||||||
|
finished_at DATETIME(3),
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_call_records_call(call_id),
|
||||||
|
UNIQUE INDEX uk_im_call_records_topic_seq(topic, seq_id),
|
||||||
|
INDEX idx_im_call_records_caller(caller_id, id DESC),
|
||||||
|
INDEX idx_im_call_records_callee(callee_id, id DESC),
|
||||||
|
INDEX idx_im_call_records_status(status, updated_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
}
|
}
|
||||||
for _, stmt := range statements {
|
for _, stmt := range statements {
|
||||||
if _, err = db.Exec(stmt); err != nil {
|
if _, err = db.Exec(stmt); err != nil {
|
||||||
@@ -88,6 +111,9 @@ func main() {
|
|||||||
if _, err = db.Exec("UPDATE tinode.im_tenant SET db_name='tinode' WHERE code='default' AND (db_name='' OR db_name IS NULL)"); err != nil {
|
if _, err = db.Exec("UPDATE tinode.im_tenant SET db_name='tinode' WHERE code='default' AND (db_name='' OR db_name IS NULL)"); err != nil {
|
||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
|
if err = dropTenantIDColumns(db, "tinode"); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
var code, dbName string
|
var code, dbName string
|
||||||
var state int
|
var state int
|
||||||
@@ -101,9 +127,107 @@ func main() {
|
|||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("tinode.im_tenant routing columns: %d/3\n", columnCount)
|
fmt.Printf("tinode.im_tenant routing columns: %d/3\n", columnCount)
|
||||||
|
|
||||||
|
rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA='tinode' AND COLUMN_NAME='tenant_id'
|
||||||
|
ORDER BY table_name`)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var tables []string
|
||||||
|
for rows.Next() {
|
||||||
|
var table string
|
||||||
|
if err = rows.Scan(&table); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
tables = append(tables, table)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if len(tables) == 0 {
|
||||||
|
fmt.Println("tinode tenant_id columns: none")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("tinode tenant_id columns: %s\n", strings.Join(tables, ","))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fatal(v any) {
|
func fatal(v any) {
|
||||||
fmt.Fprintln(os.Stderr, v)
|
fmt.Fprintln(os.Stderr, v)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dropTenantIDColumns(db *sql.DB, schema string) error {
|
||||||
|
fks, err := queryPairs(db, `SELECT DISTINCT table_name, constraint_name
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL`,
|
||||||
|
schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fk := range fks {
|
||||||
|
if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(fk[0]) + " DROP FOREIGN KEY " + quoteIdent(fk[1])); err != nil {
|
||||||
|
return fmt.Errorf("drop foreign key %s.%s: %w", fk[0], fk[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexes, err := queryPairs(db, `SELECT DISTINCT table_name, index_name
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY'`,
|
||||||
|
schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, idx := range indexes {
|
||||||
|
if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(idx[0]) + " DROP INDEX " + quoteIdent(idx[1])); err != nil {
|
||||||
|
return fmt.Errorf("drop index %s.%s: %w", idx[0], idx[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id'
|
||||||
|
ORDER BY table_name`, schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var tables []string
|
||||||
|
for rows.Next() {
|
||||||
|
var table string
|
||||||
|
if err = rows.Scan(&table); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tables = append(tables, table)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, table := range tables {
|
||||||
|
if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(table) + " DROP COLUMN tenant_id"); err != nil {
|
||||||
|
return fmt.Errorf("drop column %s.tenant_id: %w", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryPairs(db *sql.DB, query string, args ...any) ([][2]string, error) {
|
||||||
|
rows, err := db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var pairs [][2]string
|
||||||
|
for rows.Next() {
|
||||||
|
var first, second string
|
||||||
|
if err = rows.Scan(&first, &second); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pairs = append(pairs, [2]string{first, second})
|
||||||
|
}
|
||||||
|
return pairs, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteIdent(name string) string {
|
||||||
|
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
|
||||||
|
}
|
||||||
|
|||||||
+146
-1
@@ -12,9 +12,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinode/chat/server/logs"
|
"github.com/tinode/chat/server/logs"
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
"github.com/tinode/chat/server/store/types"
|
"github.com/tinode/chat/server/store/types"
|
||||||
jcr "github.com/tinode/jsonco"
|
jcr "github.com/tinode/jsonco"
|
||||||
)
|
)
|
||||||
@@ -45,6 +47,9 @@ const (
|
|||||||
constCallMsgMissed = "missed"
|
constCallMsgMissed = "missed"
|
||||||
// Call is declined (the callee hung up before picking up).
|
// Call is declined (the callee hung up before picking up).
|
||||||
constCallMsgDeclined = "declined"
|
constCallMsgDeclined = "declined"
|
||||||
|
|
||||||
|
constCallTypeAudio = "audio"
|
||||||
|
constCallTypeVideo = "video"
|
||||||
)
|
)
|
||||||
|
|
||||||
type callConfig struct {
|
type callConfig struct {
|
||||||
@@ -86,10 +91,127 @@ type videoCall struct {
|
|||||||
content any
|
content any
|
||||||
// Call message content mime type.
|
// Call message content mime type.
|
||||||
contentMime any
|
contentMime any
|
||||||
|
// Product-level call identifier.
|
||||||
|
callID string
|
||||||
|
// Product-level call type: audio or video.
|
||||||
|
callType string
|
||||||
// Time when the call was accepted.
|
// Time when the call was accepted.
|
||||||
acceptedAt time.Time
|
acceptedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCallInvite(head map[string]any, content any) (map[string]any, any, error) {
|
||||||
|
if head == nil {
|
||||||
|
return nil, nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
state, _ := head["webrtc"].(string)
|
||||||
|
if state != "started" {
|
||||||
|
return nil, nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
call, ok := content.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
if typed, typedOK := content.(types.KVMap); typedOK {
|
||||||
|
call = map[string]any(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(call) == 0 {
|
||||||
|
return nil, nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
callID, _ := call["callId"].(string)
|
||||||
|
callType, _ := call["callType"].(string)
|
||||||
|
if callType == "" {
|
||||||
|
callType, _ = call["media_type"].(string)
|
||||||
|
}
|
||||||
|
callType = strings.ToLower(strings.TrimSpace(callType))
|
||||||
|
if strings.TrimSpace(callID) == "" || (callType != constCallTypeAudio && callType != constCallTypeVideo) {
|
||||||
|
return nil, nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
normalizedHead := make(map[string]any, len(head)+2)
|
||||||
|
for key, value := range head {
|
||||||
|
normalizedHead[key] = value
|
||||||
|
}
|
||||||
|
normalizedHead["webrtc"] = "started"
|
||||||
|
normalizedHead["x-im-type"] = "call"
|
||||||
|
if _, ok := normalizedHead["mime"]; !ok {
|
||||||
|
normalizedHead["mime"] = "application/json"
|
||||||
|
}
|
||||||
|
normalizedContent := make(map[string]any, len(call)+3)
|
||||||
|
for key, value := range call {
|
||||||
|
normalizedContent[key] = value
|
||||||
|
}
|
||||||
|
normalizedContent["callId"] = strings.TrimSpace(callID)
|
||||||
|
normalizedContent["callType"] = callType
|
||||||
|
normalizedContent["audio"] = true
|
||||||
|
normalizedContent["video"] = callType == constCallTypeVideo
|
||||||
|
return normalizedHead, normalizedContent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callPayloadString(payload any, key string) string {
|
||||||
|
data, ok := payload.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
if typed, typedOK := payload.(types.KVMap); typedOK {
|
||||||
|
data = map[string]any(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
value, _ := data[key].(string)
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callContentString(content any, key string) string {
|
||||||
|
return callPayloadString(content, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (call *videoCall) matchesPayload(payload any) bool {
|
||||||
|
payloadCallID := callPayloadString(payload, "callId")
|
||||||
|
return payloadCallID == "" || payloadCallID == call.callID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Topic) auditCallCreate(call *videoCall, caller types.Uid) {
|
||||||
|
if store.Calls == nil || call == nil || call.callID == "" || caller.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callee := t.p2pOtherUser(caller)
|
||||||
|
if callee.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := types.TimeNow()
|
||||||
|
if err := store.Calls.Create(t.tenantID, &types.CallRecord{
|
||||||
|
TenantID: t.tenantID,
|
||||||
|
CallID: call.callID,
|
||||||
|
Topic: t.name,
|
||||||
|
SeqID: call.seq,
|
||||||
|
CallerID: caller,
|
||||||
|
CalleeID: callee,
|
||||||
|
CallType: call.callType,
|
||||||
|
Status: types.CallStatusStarted,
|
||||||
|
StartedAt: now,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}); err != nil {
|
||||||
|
logs.Warn.Printf("topic[%s]: failed to audit call start %s: %v", t.name, call.callID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Topic) auditCallAccept(call *videoCall, acceptedAt time.Time) {
|
||||||
|
if store.Calls == nil || call == nil || call.callID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := store.Calls.Accept(t.tenantID, call.callID, call.seq, acceptedAt); err != nil {
|
||||||
|
logs.Warn.Printf("topic[%s]: failed to audit call accept %s: %v", t.name, call.callID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Topic) auditCallFinish(call *videoCall, status string, durationMS int64, finishedAt time.Time) {
|
||||||
|
if store.Calls == nil || call == nil || call.callID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := store.Calls.Finish(t.tenantID, call.callID, call.seq, status, durationMS, finishedAt); err != nil {
|
||||||
|
logs.Warn.Printf("topic[%s]: failed to audit call finish %s: %v", t.name, call.callID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// callPartySession returns a session to be stored in the call party data.
|
// callPartySession returns a session to be stored in the call party data.
|
||||||
func callPartySession(sess *Session) *Session {
|
func callPartySession(sess *Session) *Session {
|
||||||
if sess.isProxy() {
|
if sess.isProxy() {
|
||||||
@@ -226,12 +348,15 @@ func (t *Topic) handleCallInvite(msg *ClientComMessage, asUid types.Uid) {
|
|||||||
seq: t.lastID,
|
seq: t.lastID,
|
||||||
content: msg.Pub.Content,
|
content: msg.Pub.Content,
|
||||||
contentMime: msg.Pub.Head["mime"],
|
contentMime: msg.Pub.Head["mime"],
|
||||||
|
callID: callContentString(msg.Pub.Content, "callId"),
|
||||||
|
callType: callContentString(msg.Pub.Content, "callType"),
|
||||||
}
|
}
|
||||||
t.currentCall.parties[msg.sess.sid] = callPartyData{
|
t.currentCall.parties[msg.sess.sid] = callPartyData{
|
||||||
uid: asUid,
|
uid: asUid,
|
||||||
isOriginator: true,
|
isOriginator: true,
|
||||||
sess: callPartySession(msg.sess),
|
sess: callPartySession(msg.sess),
|
||||||
}
|
}
|
||||||
|
t.auditCallCreate(t.currentCall, asUid)
|
||||||
// Wait for constCallEstablishmentTimeout for the other side to accept the call.
|
// Wait for constCallEstablishmentTimeout for the other side to accept the call.
|
||||||
t.callEstablishmentTimer.Reset(time.Duration(globals.callEstablishmentTimeout) * time.Second)
|
t.callEstablishmentTimer.Reset(time.Duration(globals.callEstablishmentTimeout) * time.Second)
|
||||||
}
|
}
|
||||||
@@ -255,6 +380,10 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
|
|||||||
logs.Info.Printf("topic[%s]: invalid seq id - current call (%d) vs received (%d)", t.name, t.currentCall.seq, call.SeqId)
|
logs.Info.Printf("topic[%s]: invalid seq id - current call (%d) vs received (%d)", t.name, t.currentCall.seq, call.SeqId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !t.currentCall.matchesPayload(call.Payload) {
|
||||||
|
logs.Info.Printf("topic[%s]: invalid call id for seq %d", t.name, t.currentCall.seq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
asUid := types.ParseUserId(msg.AsUser)
|
asUid := types.ParseUserId(msg.AsUser)
|
||||||
|
|
||||||
@@ -307,6 +436,7 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
|
|||||||
sess: callPartySession(msg.sess),
|
sess: callPartySession(msg.sess),
|
||||||
}
|
}
|
||||||
t.currentCall.acceptedAt = time.Now()
|
t.currentCall.acceptedAt = time.Now()
|
||||||
|
t.auditCallAccept(t.currentCall, t.currentCall.acceptedAt)
|
||||||
|
|
||||||
// Notify other clients that the call has been accepted.
|
// Notify other clients that the call has been accepted.
|
||||||
t.infoCallSubsOffline(msg.AsUser, asUid, call.Event, t.currentCall.seq, call.Payload, msg.sess.sid, false)
|
t.infoCallSubsOffline(msg.AsUser, asUid, call.Event, t.currentCall.seq, call.Payload, msg.sess.sid, false)
|
||||||
@@ -382,10 +512,11 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
|||||||
originatorUid, _ := t.getCallOriginator()
|
originatorUid, _ := t.getCallOriginator()
|
||||||
var replaceWith string
|
var replaceWith string
|
||||||
var callDuration int64
|
var callDuration int64
|
||||||
|
finishedAt := time.Now()
|
||||||
if from != "" && len(t.currentCall.parties) == 2 {
|
if from != "" && len(t.currentCall.parties) == 2 {
|
||||||
// This is a call in progress.
|
// This is a call in progress.
|
||||||
replaceWith = constCallMsgFinished
|
replaceWith = constCallMsgFinished
|
||||||
callDuration = time.Since(t.currentCall.acceptedAt).Milliseconds()
|
callDuration = finishedAt.Sub(t.currentCall.acceptedAt).Milliseconds()
|
||||||
} else {
|
} else {
|
||||||
if from != "" {
|
if from != "" {
|
||||||
// User originated hang-up.
|
// User originated hang-up.
|
||||||
@@ -418,6 +549,7 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
|||||||
if err := t.saveAndBroadcastMessage(&msgCopy, originatorUid, false, nil, head, t.currentCall.content); err != nil {
|
if err := t.saveAndBroadcastMessage(&msgCopy, originatorUid, false, nil, head, t.currentCall.content); err != nil {
|
||||||
logs.Err.Printf("topic[%s]: failed to write finalizing message for call seq id %d - '%s'", t.name, t.currentCall.seq, err)
|
logs.Err.Printf("topic[%s]: failed to write finalizing message for call seq id %d - '%s'", t.name, t.currentCall.seq, err)
|
||||||
}
|
}
|
||||||
|
t.auditCallFinish(t.currentCall, callFinalStatus(replaceWith), callDuration, finishedAt)
|
||||||
|
|
||||||
// Send {info} hangup event to the subscribed sessions.
|
// Send {info} hangup event to the subscribed sessions.
|
||||||
t.broadcastToSessions(t.currentCall.infoMessage(constCallEventHangUp))
|
t.broadcastToSessions(t.currentCall.infoMessage(constCallEventHangUp))
|
||||||
@@ -429,6 +561,19 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
|||||||
t.currentCall = nil
|
t.currentCall = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func callFinalStatus(replaceWith string) string {
|
||||||
|
switch replaceWith {
|
||||||
|
case constCallMsgFinished:
|
||||||
|
return types.CallStatusFinished
|
||||||
|
case constCallMsgMissed:
|
||||||
|
return types.CallStatusMissed
|
||||||
|
case constCallMsgDeclined:
|
||||||
|
return types.CallStatusDeclined
|
||||||
|
default:
|
||||||
|
return types.CallStatusDisconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Server initiated call termination.
|
// Server initiated call termination.
|
||||||
func (t *Topic) terminateCallInProgress(callDidTimeout bool) {
|
func (t *Topic) terminateCallInProgress(callDidTimeout bool) {
|
||||||
if t.currentCall == nil {
|
if t.currentCall == nil {
|
||||||
|
|||||||
@@ -289,6 +289,10 @@ type TenantBusinessAdapter interface {
|
|||||||
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
||||||
TenantMessageSearch(t.TenantID, string, t.Uid, string, string, string, int) ([]t.Message, string, error)
|
TenantMessageSearch(t.TenantID, string, t.Uid, string, string, string, int) ([]t.Message, string, error)
|
||||||
|
|
||||||
|
TenantCallCreate(t.TenantID, *t.CallRecord) error
|
||||||
|
TenantCallAccept(t.TenantID, string, int, time.Time) error
|
||||||
|
TenantCallFinish(t.TenantID, string, int, string, int64, time.Time) error
|
||||||
|
|
||||||
TenantFavoriteCreate(t.TenantID, *t.FavoriteItem) (*t.FavoriteItem, error)
|
TenantFavoriteCreate(t.TenantID, *t.FavoriteItem) (*t.FavoriteItem, error)
|
||||||
TenantFavoriteList(t.TenantID, t.Uid, string, string, string, int) ([]t.FavoriteItem, string, error)
|
TenantFavoriteList(t.TenantID, t.Uid, string, string, string, int) ([]t.FavoriteItem, string, error)
|
||||||
TenantFavoriteGet(t.TenantID, t.Uid, string) (*t.FavoriteItem, error)
|
TenantFavoriteGet(t.TenantID, t.Uid, string) (*t.FavoriteItem, error)
|
||||||
|
|||||||
@@ -791,6 +791,9 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
if err = createIMRelationTables(tx); err != nil {
|
if err = createIMRelationTables(tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = createIMCallTables(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Deletion log
|
// Deletion log
|
||||||
if _, err = tx.Exec(
|
if _, err = tx.Exec(
|
||||||
@@ -919,6 +922,34 @@ func createIMNotificationTables(tx *sql.Tx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createIMCallTables(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(
|
||||||
|
`CREATE TABLE im_call_records(
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
call_id VARCHAR(64) NOT NULL,
|
||||||
|
topic CHAR(25) NOT NULL,
|
||||||
|
seq_id INT NOT NULL,
|
||||||
|
caller_id BIGINT NOT NULL,
|
||||||
|
callee_id BIGINT NOT NULL,
|
||||||
|
call_type VARCHAR(16) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
end_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
started_at DATETIME(3) NOT NULL,
|
||||||
|
accepted_at DATETIME(3),
|
||||||
|
finished_at DATETIME(3),
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_call_records_call(call_id),
|
||||||
|
UNIQUE INDEX uk_im_call_records_topic_seq(topic, seq_id),
|
||||||
|
INDEX idx_im_call_records_caller(caller_id, id DESC),
|
||||||
|
INDEX idx_im_call_records_callee(callee_id, id DESC),
|
||||||
|
INDEX idx_im_call_records_status(status, updated_at)
|
||||||
|
)`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func ensureTenantRoutingColumns(a *adapter) error {
|
func ensureTenantRoutingColumns(a *adapter) error {
|
||||||
statements := []string{
|
statements := []string{
|
||||||
"ALTER TABLE im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
|
"ALTER TABLE im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
|
||||||
@@ -1258,7 +1289,7 @@ func (a *adapter) TenantAuthDelScheme(tenantID t.TenantID, uid t.Uid, scheme str
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, "DELETE FROM auth WHERE tenant_id=? AND userid=? AND scheme=?",
|
_, err = a.db.ExecContext(ctx, "DELETE FROM auth WHERE tenant_id=? AND userid=? AND scheme=?",
|
||||||
tenantID, store.DecodeUid(uid), scheme)
|
tenantID, store.DecodeUid(uid), scheme)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -3942,7 +3973,7 @@ func (a *adapter) TenantCredFail(tenantID t.TenantID, uid t.Uid, method string)
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, "UPDATE credentials SET updatedat=?,retries=retries+1 "+
|
_, err = a.db.ExecContext(ctx, "UPDATE credentials SET updatedat=?,retries=retries+1 "+
|
||||||
"WHERE tenant_id=? AND userid=? AND method=? AND done=FALSE",
|
"WHERE tenant_id=? AND userid=? AND method=? AND done=FALSE",
|
||||||
t.TimeNow(), tenantID, store.DecodeUid(uid), method)
|
t.TimeNow(), tenantID, store.DecodeUid(uid), method)
|
||||||
return err
|
return err
|
||||||
@@ -3983,7 +4014,7 @@ func (a *adapter) TenantCredGetActive(tenantID t.TenantID, uid t.Uid, method str
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var cred t.Credential
|
var cred t.Credential
|
||||||
err := a.db.GetContext(ctx, &cred, "SELECT createdat,updatedat,method,value,resp,done,retries "+
|
err = a.db.GetContext(ctx, &cred, "SELECT createdat,updatedat,method,value,resp,done,retries "+
|
||||||
"FROM credentials WHERE tenant_id=? AND userid=? AND deletedat IS NULL AND method=? AND done=FALSE",
|
"FROM credentials WHERE tenant_id=? AND userid=? AND deletedat IS NULL AND method=? AND done=FALSE",
|
||||||
tenantID, store.DecodeUid(uid), method)
|
tenantID, store.DecodeUid(uid), method)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -4292,6 +4323,57 @@ func (a *adapter) FileLinkAttachments(topic string, userId, msgId t.Uid, fids []
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
|
||||||
|
if uid.IsZero() {
|
||||||
|
return false, t.ErrMalformed
|
||||||
|
}
|
||||||
|
fileID := t.ParseUid(fid)
|
||||||
|
if fileID.IsZero() {
|
||||||
|
return false, t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
var allowed int
|
||||||
|
err := a.db.GetContext(ctx, &allowed, `
|
||||||
|
SELECT COUNT(*) FROM fileuploads AS fu
|
||||||
|
WHERE fu.id=? AND fu.status=? AND (
|
||||||
|
fu.userid=?
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM filemsglinks AS fml
|
||||||
|
WHERE fml.fileid=fu.id AND fml.userid=?
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM filemsglinks AS fml
|
||||||
|
INNER JOIN subscriptions AS s
|
||||||
|
ON s.topic=fml.topic AND s.userid=?
|
||||||
|
WHERE fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||||
|
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM filemsglinks AS fml
|
||||||
|
INNER JOIN messages AS m
|
||||||
|
ON m.id=fml.msgid
|
||||||
|
INNER JOIN subscriptions AS s
|
||||||
|
ON s.topic=m.topic AND s.userid=?
|
||||||
|
WHERE fml.fileid=fu.id AND fml.msgid IS NOT NULL
|
||||||
|
AND m.deletedat IS NULL AND m.recalled_at IS NULL
|
||||||
|
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM dellog AS d
|
||||||
|
WHERE d.topic=m.topic AND d.deletedfor=?
|
||||||
|
AND d.low<=m.seqid AND (d.hi=0 OR d.hi>m.seqid)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) LIMIT 1`,
|
||||||
|
store.DecodeUid(fileID), t.UploadCompleted, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return allowed > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// PCacheGet reads a persistet cache entry.
|
// PCacheGet reads a persistet cache entry.
|
||||||
func (a *adapter) PCacheGet(key string) (string, error) {
|
func (a *adapter) PCacheGet(key string) (string, error) {
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ func (a *adapter) TenantUserCreate(id t.TenantID, user *t.User) error {
|
|||||||
if err != nil || user == nil || user.TenantID != id {
|
if err != nil || user == nil || user.TenantID != id {
|
||||||
return t.ErrMalformed
|
return t.ErrMalformed
|
||||||
}
|
}
|
||||||
|
if tenantAdp.tenantScopedDB {
|
||||||
|
return tenantAdp.UserCreate(user)
|
||||||
|
}
|
||||||
return tenantAdp.UserCreate(user)
|
return tenantAdp.UserCreate(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,12 +142,16 @@ func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) {
|
|||||||
user, err := tenantAdp.UserGet(uid)
|
user, err := tenantAdp.UserGet(uid)
|
||||||
return stampTenantUser(id, user), err
|
return stampTenantUser(id, user), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
user, err := a.UserGet(uid)
|
||||||
|
return stampTenantUser(id, user), err
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var user t.User
|
var user t.User
|
||||||
err := a.db.GetContext(ctx, &user, "SELECT * FROM users WHERE tenant_id=? AND id=? AND state!=?",
|
err = a.db.GetContext(ctx, &user, "SELECT * FROM users WHERE tenant_id=? AND id=? AND state!=?",
|
||||||
id, decodeUID(uid), t.StateDeleted)
|
id, decodeUID(uid), t.StateDeleted)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -165,6 +172,10 @@ func (a *adapter) TenantUserGetAll(id t.TenantID, uids ...t.Uid) ([]t.User, erro
|
|||||||
users, err := tenantAdp.UserGetAll(uids...)
|
users, err := tenantAdp.UserGetAll(uids...)
|
||||||
return stampTenantUsers(id, users), err
|
return stampTenantUsers(id, users), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
users, err := a.UserGetAll(uids...)
|
||||||
|
return stampTenantUsers(id, users), err
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -185,7 +196,13 @@ func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.Tena
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
q, params, _ := sqlx.In("SELECT * FROM users WHERE tenant_id=? AND id IN (?) AND state!=?", id, args, t.StateDeleted)
|
var q string
|
||||||
|
var params []any
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
q, params, _ = sqlx.In("SELECT * FROM users WHERE id IN (?) AND state!=?", args, t.StateDeleted)
|
||||||
|
} else {
|
||||||
|
q, params, _ = sqlx.In("SELECT * FROM users WHERE tenant_id=? AND id IN (?) AND state!=?", id, args, t.StateDeleted)
|
||||||
|
}
|
||||||
query := a.db.Rebind(q)
|
query := a.db.Rebind(q)
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
var err error
|
var err error
|
||||||
@@ -219,6 +236,9 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserDelete(uid, hard)
|
return tenantAdp.UserDelete(uid, hard)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.UserDelete(uid, hard)
|
||||||
|
}
|
||||||
decoded := decodeUID(uid)
|
decoded := decodeUID(uid)
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
@@ -351,6 +371,9 @@ func (a *adapter) TenantUserUpdate(id t.TenantID, uid t.Uid, update map[string]a
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserUpdate(uid, update)
|
return tenantAdp.UserUpdate(uid, update)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.UserUpdate(uid, update)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -399,6 +422,9 @@ func (a *adapter) TenantUserUpdateTags(id t.TenantID, uid t.Uid, add, remove, re
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserUpdateTags(uid, add, remove, reset)
|
return tenantAdp.UserUpdateTags(uid, add, remove, reset)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.UserUpdateTags(uid, add, remove, reset)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -446,6 +472,9 @@ func (a *adapter) TenantUserUnreadCount(id t.TenantID, uids ...t.Uid) (map[t.Uid
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserUnreadCount(uids...)
|
return tenantAdp.UserUnreadCount(uids...)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.UserUnreadCount(uids...)
|
||||||
|
}
|
||||||
counts := make(map[t.Uid]int, len(uids))
|
counts := make(map[t.Uid]int, len(uids))
|
||||||
var decoded []any
|
var decoded []any
|
||||||
for _, uid := range uids {
|
for _, uid := range uids {
|
||||||
@@ -489,6 +518,9 @@ func (a *adapter) TenantUserGetUnvalidated(id t.TenantID, before time.Time, limi
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserGetUnvalidated(before, limit)
|
return tenantAdp.UserGetUnvalidated(before, limit)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.UserGetUnvalidated(before, limit)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -537,6 +569,9 @@ func (a *adapter) TenantTopicCreate(id t.TenantID, topic *t.Topic) error {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicCreate(topic)
|
return tenantAdp.TopicCreate(topic)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicCreate(topic)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -564,6 +599,9 @@ func (a *adapter) TenantTopicCreateP2P(id t.TenantID, one, two *t.Subscription)
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicCreateP2P(one, two)
|
return tenantAdp.TopicCreateP2P(one, two)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicCreateP2P(one, two)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -601,12 +639,16 @@ func (a *adapter) TenantTopicGet(id t.TenantID, topic string) (*t.Topic, error)
|
|||||||
top, err := tenantAdp.TopicGet(topic)
|
top, err := tenantAdp.TopicGet(topic)
|
||||||
return stampTenantTopic(id, top), err
|
return stampTenantTopic(id, top), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
top, err := a.TopicGet(topic)
|
||||||
|
return stampTenantTopic(id, top), err
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var top t.Topic
|
var top t.Topic
|
||||||
err := a.db.GetContext(ctx, &top,
|
err = a.db.GetContext(ctx, &top,
|
||||||
"SELECT tenant_id,createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,public,trusted,tags,aux "+
|
"SELECT tenant_id,createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,public,trusted,tags,aux "+
|
||||||
"FROM topics WHERE tenant_id=? AND name=?", id, topic)
|
"FROM topics WHERE tenant_id=? AND name=?", id, topic)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -642,6 +684,10 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool
|
|||||||
subs, err := tenantAdp.TopicsForUser(uid, keepDeleted, opts)
|
subs, err := tenantAdp.TopicsForUser(uid, keepDeleted, opts)
|
||||||
return stampTenantSubs(id, subs), err
|
return stampTenantSubs(id, subs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
subs, err := a.TopicsForUser(uid, keepDeleted, opts)
|
||||||
|
return stampTenantSubs(id, subs), err
|
||||||
|
}
|
||||||
q := `SELECT tenant_id,createdat,updatedat,deletedat,topic,delid,recvseqid,
|
q := `SELECT tenant_id,createdat,updatedat,deletedat,topic,delid,recvseqid,
|
||||||
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND userid=?`
|
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND userid=?`
|
||||||
args := []any{id, decodeUID(uid)}
|
args := []any{id, decodeUID(uid)}
|
||||||
@@ -805,6 +851,10 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b
|
|||||||
subs, err := tenantAdp.UsersForTopic(topic, keepDeleted, opts)
|
subs, err := tenantAdp.UsersForTopic(topic, keepDeleted, opts)
|
||||||
return stampTenantSubs(id, subs), err
|
return stampTenantSubs(id, subs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
subs, err := a.UsersForTopic(topic, keepDeleted, opts)
|
||||||
|
return stampTenantSubs(id, subs), err
|
||||||
|
}
|
||||||
tcat := t.GetTopicCat(topic)
|
tcat := t.GetTopicCat(topic)
|
||||||
q := `SELECT s.tenant_id,s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid,
|
q := `SELECT s.tenant_id,s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid,
|
||||||
s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private
|
s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private
|
||||||
@@ -924,6 +974,9 @@ func (a *adapter) TenantOwnTopics(id t.TenantID, uid t.Uid) ([]string, error) {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.OwnTopics(uid)
|
return tenantAdp.OwnTopics(uid)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.OwnTopics(uid)
|
||||||
|
}
|
||||||
return a.tenantTopicNames("SELECT name FROM topics WHERE tenant_id=? AND owner=? AND state!=?",
|
return a.tenantTopicNames("SELECT name FROM topics WHERE tenant_id=? AND owner=? AND state!=?",
|
||||||
id, decodeUID(uid), t.StateDeleted)
|
id, decodeUID(uid), t.StateDeleted)
|
||||||
}
|
}
|
||||||
@@ -936,6 +989,9 @@ func (a *adapter) TenantChannelsForUser(id t.TenantID, uid t.Uid) ([]string, err
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.ChannelsForUser(uid)
|
return tenantAdp.ChannelsForUser(uid)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.ChannelsForUser(uid)
|
||||||
|
}
|
||||||
return a.tenantTopicNames("SELECT topic FROM subscriptions WHERE tenant_id=? AND userid=? AND topic LIKE 'chn%' "+
|
return a.tenantTopicNames("SELECT topic FROM subscriptions WHERE tenant_id=? AND userid=? AND topic LIKE 'chn%' "+
|
||||||
"AND INSTR(modewant,'P')>0 AND INSTR(modegiven,'P')>0 AND deletedat IS NULL", id, decodeUID(uid))
|
"AND INSTR(modewant,'P')>0 AND INSTR(modegiven,'P')>0 AND deletedat IS NULL", id, decodeUID(uid))
|
||||||
}
|
}
|
||||||
@@ -953,6 +1009,9 @@ func (a *adapter) TenantTopicShare(id t.TenantID, topic string, subs []*t.Subscr
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicShare(topic, subs)
|
return tenantAdp.TopicShare(topic, subs)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicShare(topic, subs)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1046,6 +1105,9 @@ func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bo
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicDelete(topic, isChan, hard)
|
return tenantAdp.TopicDelete(topic, isChan, hard)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicDelete(topic, isChan, hard)
|
||||||
|
}
|
||||||
topics := []any{topic}
|
topics := []any{topic}
|
||||||
if isChan {
|
if isChan {
|
||||||
topics = append(topics, t.GrpToChn(topic))
|
topics = append(topics, t.GrpToChn(topic))
|
||||||
@@ -1099,11 +1161,14 @@ func (a *adapter) TenantTopicUpdateOnMessage(id t.TenantID, topic string, msg *t
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicUpdateOnMessage(topic, msg)
|
return tenantAdp.TopicUpdateOnMessage(topic, msg)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicUpdateOnMessage(topic, msg)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, "UPDATE topics SET seqid=?,touchedat=? WHERE tenant_id=? AND name=?",
|
_, err = a.db.ExecContext(ctx, "UPDATE topics SET seqid=?,touchedat=? WHERE tenant_id=? AND name=?",
|
||||||
msg.SeqId, msg.CreatedAt, id, topic)
|
msg.SeqId, msg.CreatedAt, id, topic)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1116,11 +1181,14 @@ func (a *adapter) TenantTopicUpdateSubCnt(id t.TenantID, topic string) error {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicUpdateSubCnt(topic)
|
return tenantAdp.TopicUpdateSubCnt(topic)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicUpdateSubCnt(topic)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx,
|
_, err = a.db.ExecContext(ctx,
|
||||||
"UPDATE topics SET subcnt=(SELECT COUNT(*) FROM subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL) WHERE tenant_id=? AND name=?",
|
"UPDATE topics SET subcnt=(SELECT COUNT(*) FROM subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL) WHERE tenant_id=? AND name=?",
|
||||||
id, topic, t.GrpToChn(topic), id, topic)
|
id, topic, t.GrpToChn(topic), id, topic)
|
||||||
return err
|
return err
|
||||||
@@ -1134,6 +1202,9 @@ func (a *adapter) TenantTopicUpdate(id t.TenantID, topic string, update map[stri
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicUpdate(topic, update)
|
return tenantAdp.TopicUpdate(topic, update)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicUpdate(topic, update)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1178,11 +1249,14 @@ func (a *adapter) TenantTopicOwnerChange(id t.TenantID, topic string, owner t.Ui
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TopicOwnerChange(topic, owner)
|
return tenantAdp.TopicOwnerChange(topic, owner)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.TopicOwnerChange(topic, owner)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, "UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decodeUID(owner), id, topic)
|
_, err = a.db.ExecContext(ctx, "UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decodeUID(owner), id, topic)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,6 +1272,13 @@ func (a *adapter) TenantSubscriptionGet(id t.TenantID, topic string, uid t.Uid,
|
|||||||
}
|
}
|
||||||
return sub, err
|
return sub, err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
sub, err := a.SubscriptionGet(topic, uid, keepDeleted)
|
||||||
|
if sub != nil {
|
||||||
|
sub.TenantID = id
|
||||||
|
}
|
||||||
|
return sub, err
|
||||||
|
}
|
||||||
query := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
query := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||||
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND topic=? AND userid=?`
|
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND topic=? AND userid=?`
|
||||||
if !keepDeleted {
|
if !keepDeleted {
|
||||||
@@ -1208,7 +1289,7 @@ func (a *adapter) TenantSubscriptionGet(id t.TenantID, topic string, uid t.Uid,
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var sub t.Subscription
|
var sub t.Subscription
|
||||||
err := a.db.GetContext(ctx, &sub, query, id, topic, decodeUID(uid))
|
err = a.db.GetContext(ctx, &sub, query, id, topic, decodeUID(uid))
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1310,10 @@ func (a *adapter) TenantSubsForUser(id t.TenantID, uid t.Uid) ([]t.Subscription,
|
|||||||
subs, err := tenantAdp.SubsForUser(uid)
|
subs, err := tenantAdp.SubsForUser(uid)
|
||||||
return stampTenantSubs(id, subs), err
|
return stampTenantSubs(id, subs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
subs, err := a.SubsForUser(uid)
|
||||||
|
return stampTenantSubs(id, subs), err
|
||||||
|
}
|
||||||
rows, err := a.db.Queryx(`SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
rows, err := a.db.Queryx(`SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||||
readseqid,modewant,modegiven FROM subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL`,
|
readseqid,modewant,modegiven FROM subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL`,
|
||||||
id, decodeUID(uid))
|
id, decodeUID(uid))
|
||||||
@@ -1257,6 +1342,10 @@ func (a *adapter) TenantSubsForTopic(id t.TenantID, topic string, keepDeleted bo
|
|||||||
subs, err := tenantAdp.SubsForTopic(topic, keepDeleted, opts)
|
subs, err := tenantAdp.SubsForTopic(topic, keepDeleted, opts)
|
||||||
return stampTenantSubs(id, subs), err
|
return stampTenantSubs(id, subs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
subs, err := a.SubsForTopic(topic, keepDeleted, opts)
|
||||||
|
return stampTenantSubs(id, subs), err
|
||||||
|
}
|
||||||
q := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
q := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||||
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND topic=?`
|
readseqid,modewant,modegiven,private FROM subscriptions WHERE tenant_id=? AND topic=?`
|
||||||
args := []any{id, topic}
|
args := []any{id, topic}
|
||||||
@@ -1305,6 +1394,9 @@ func (a *adapter) TenantSubsUpdate(id t.TenantID, topic string, uid t.Uid, updat
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.SubsUpdate(topic, uid, update)
|
return tenantAdp.SubsUpdate(topic, uid, update)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.SubsUpdate(topic, uid, update)
|
||||||
|
}
|
||||||
cols, args := common.UpdateByMap(update)
|
cols, args := common.UpdateByMap(update)
|
||||||
q := "UPDATE subscriptions SET " + strings.Join(cols, ",") + " WHERE tenant_id=? AND topic=?"
|
q := "UPDATE subscriptions SET " + strings.Join(cols, ",") + " WHERE tenant_id=? AND topic=?"
|
||||||
args = append(args, id, topic)
|
args = append(args, id, topic)
|
||||||
@@ -1316,7 +1408,7 @@ func (a *adapter) TenantSubsUpdate(id t.TenantID, topic string, uid t.Uid, updat
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, q, args...)
|
_, err = a.db.ExecContext(ctx, q, args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,6 +1420,9 @@ func (a *adapter) TenantSubsDelete(id t.TenantID, topic string, uid t.Uid) error
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.SubsDelete(topic, uid)
|
return tenantAdp.SubsDelete(topic, uid)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.SubsDelete(topic, uid)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1411,6 +1506,10 @@ func (a *adapter) TenantFind(id t.TenantID, caller, promoPrefix string, req [][]
|
|||||||
subs, err := tenantAdp.Find(caller, promoPrefix, req, opt, activeOnly)
|
subs, err := tenantAdp.Find(caller, promoPrefix, req, opt, activeOnly)
|
||||||
return stampTenantSubs(id, subs), err
|
return stampTenantSubs(id, subs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
subs, err := a.Find(caller, promoPrefix, req, opt, activeOnly)
|
||||||
|
return stampTenantSubs(id, subs), err
|
||||||
|
}
|
||||||
allReq := t.FlattenDoubleSlice(req)
|
allReq := t.FlattenDoubleSlice(req)
|
||||||
allTags := append(append([]string{}, allReq...), opt...)
|
allTags := append(append([]string{}, allReq...), opt...)
|
||||||
if len(allTags) == 0 {
|
if len(allTags) == 0 {
|
||||||
@@ -1513,6 +1612,9 @@ func (a *adapter) TenantFindOne(id t.TenantID, tag string) (string, error) {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.FindOne(tag)
|
return tenantAdp.FindOne(tag)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.FindOne(tag)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1546,6 +1648,9 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error {
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.MessageSave(msg)
|
return tenantAdp.MessageSave(msg)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.MessageSave(msg)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1729,6 +1834,10 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
|
|||||||
msgs, err := tenantAdp.MessageGetAll(topic, uid, opts)
|
msgs, err := tenantAdp.MessageGetAll(topic, uid, opts)
|
||||||
return stampTenantMessages(id, msgs), err
|
return stampTenantMessages(id, msgs), err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
msgs, err := a.MessageGetAll(topic, uid, opts)
|
||||||
|
return stampTenantMessages(id, msgs), err
|
||||||
|
}
|
||||||
limit := a.maxMessageResults
|
limit := a.maxMessageResults
|
||||||
args := []any{id, topic, id, decodeUID(uid)}
|
args := []any{id, topic, id, decodeUID(uid)}
|
||||||
seq := ""
|
seq := ""
|
||||||
@@ -1856,6 +1965,95 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
|||||||
return msgs, nextCursor, nil
|
return msgs, nextCursor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantCallCreate(id t.TenantID, record *t.CallRecord) error {
|
||||||
|
if !a.tenantScopedDB {
|
||||||
|
tenantAdp, err := a.routedTenantAdapter(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tenantAdp != a {
|
||||||
|
return tenantAdp.TenantCallCreate(id, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validTenant(id); err != nil || record == nil || record.TenantID != id || record.CallID == "" ||
|
||||||
|
record.Topic == "" || record.SeqID <= 0 || record.CallerID.IsZero() || record.CalleeID.IsZero() ||
|
||||||
|
record.CallType == "" {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
now := t.TimeNow()
|
||||||
|
if record.StartedAt.IsZero() {
|
||||||
|
record.StartedAt = now
|
||||||
|
}
|
||||||
|
record.CreatedAt = now
|
||||||
|
record.UpdatedAt = now
|
||||||
|
if record.Status == "" {
|
||||||
|
record.Status = t.CallStatusStarted
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO im_call_records
|
||||||
|
(call_id,topic,seq_id,caller_id,callee_id,call_type,status,end_reason,duration_ms,started_at,accepted_at,finished_at,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)`,
|
||||||
|
record.CallID, record.Topic, record.SeqID, decodeUID(record.CallerID), decodeUID(record.CalleeID),
|
||||||
|
record.CallType, record.Status, record.EndReason, record.DurationMS, record.StartedAt, record.AcceptedAt,
|
||||||
|
record.FinishedAt, record.CreatedAt, record.UpdatedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantCallAccept(id t.TenantID, callID string, seq int, acceptedAt time.Time) error {
|
||||||
|
if !a.tenantScopedDB {
|
||||||
|
tenantAdp, err := a.routedTenantAdapter(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tenantAdp != a {
|
||||||
|
return tenantAdp.TenantCallAccept(id, callID, seq, acceptedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validTenant(id); err != nil || callID == "" || seq <= 0 || acceptedAt.IsZero() {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
`UPDATE im_call_records
|
||||||
|
SET status=?,accepted_at=?,updated_at=?
|
||||||
|
WHERE call_id=? AND seq_id=? AND status=?`,
|
||||||
|
t.CallStatusAccepted, acceptedAt, acceptedAt, callID, seq, t.CallStatusStarted)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantCallFinish(id t.TenantID, callID string, seq int, status string, durationMS int64, finishedAt time.Time) error {
|
||||||
|
if !a.tenantScopedDB {
|
||||||
|
tenantAdp, err := a.routedTenantAdapter(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tenantAdp != a {
|
||||||
|
return tenantAdp.TenantCallFinish(id, callID, seq, status, durationMS, finishedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validTenant(id); err != nil || callID == "" || seq <= 0 || finishedAt.IsZero() || durationMS < 0 {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
_, err := a.db.ExecContext(ctx,
|
||||||
|
`UPDATE im_call_records
|
||||||
|
SET status=?,end_reason=?,duration_ms=?,finished_at=?,updated_at=?
|
||||||
|
WHERE call_id=? AND seq_id=?`,
|
||||||
|
status, status, durationMS, finishedAt, finishedAt, callID, seq)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int, suffix string) (*t.Message, error) {
|
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int, suffix string) (*t.Message, error) {
|
||||||
row := struct {
|
row := struct {
|
||||||
t.Message
|
t.Message
|
||||||
@@ -2071,6 +2269,9 @@ func (a *adapter) TenantMessageDeleteList(id t.TenantID, topic string, del *t.De
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.MessageDeleteList(topic, del)
|
return tenantAdp.MessageDeleteList(topic, del)
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
return a.MessageDeleteList(topic, del)
|
||||||
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
ctx, cancel := a.getContextForTx()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -2102,6 +2303,13 @@ func (a *adapter) TenantMessageGetDeleted(id t.TenantID, topic string, uid t.Uid
|
|||||||
}
|
}
|
||||||
return deleted, err
|
return deleted, err
|
||||||
}
|
}
|
||||||
|
if a.tenantScopedDB {
|
||||||
|
deleted, err := a.MessageGetDeleted(topic, uid, opts)
|
||||||
|
for i := range deleted {
|
||||||
|
deleted[i].TenantID = id
|
||||||
|
}
|
||||||
|
return deleted, err
|
||||||
|
}
|
||||||
limit, lower, upper := a.maxResults, 0, 1<<31-1
|
limit, lower, upper := a.maxResults, 0, 1<<31-1
|
||||||
if opts != nil {
|
if opts != nil {
|
||||||
if opts.Since > 0 {
|
if opts.Since > 0 {
|
||||||
@@ -2312,7 +2520,7 @@ func (a *adapter) TenantFileStartUpload(id t.TenantID, fd *t.FileDef) error {
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx,
|
_, err = a.db.ExecContext(ctx,
|
||||||
"INSERT INTO fileuploads(tenant_id,id,createdat,updatedat,userid,status,mimetype,size,etag,location) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO fileuploads(tenant_id,id,createdat,updatedat,userid,status,mimetype,size,etag,location) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
||||||
id, decodeUID(fd.Uid()), fd.CreatedAt, fd.UpdatedAt, user, fd.Status, fd.MimeType, fd.Size, fd.ETag, fd.Location)
|
id, decodeUID(fd.Uid()), fd.CreatedAt, fd.UpdatedAt, user, fd.Status, fd.MimeType, fd.Size, fd.ETag, fd.Location)
|
||||||
return err
|
return err
|
||||||
@@ -2384,7 +2592,7 @@ func (a *adapter) TenantFileGet(id t.TenantID, fid string) (*t.FileDef, error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var fd t.FileDef
|
var fd t.FileDef
|
||||||
err := a.db.GetContext(ctx, &fd,
|
err = a.db.GetContext(ctx, &fd,
|
||||||
"SELECT tenant_id,id,createdat,updatedat,userid AS user,status,mimetype,size,IFNULL(etag,'') AS etag,location "+
|
"SELECT tenant_id,id,createdat,updatedat,userid AS user,status,mimetype,size,IFNULL(etag,'') AS etag,location "+
|
||||||
"FROM fileuploads WHERE tenant_id=? AND id=?", id, decodeUID(fileID))
|
"FROM fileuploads WHERE tenant_id=? AND id=?", id, decodeUID(fileID))
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -2544,7 +2752,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var allowed int
|
var allowed int
|
||||||
err := a.db.GetContext(ctx, &allowed, `
|
err = a.db.GetContext(ctx, &allowed, `
|
||||||
SELECT COUNT(*) FROM fileuploads AS fu
|
SELECT COUNT(*) FROM fileuploads AS fu
|
||||||
WHERE fu.tenant_id=? AND fu.id=? AND fu.status=? AND (
|
WHERE fu.tenant_id=? AND fu.id=? AND fu.status=? AND (
|
||||||
fu.userid=?
|
fu.userid=?
|
||||||
@@ -3638,7 +3846,12 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
topicRow := &t.Topic{TenantID: id, ObjHeader: t.ObjHeader{Id: topic, CreatedAt: handledAt, UpdatedAt: handledAt}, TouchedAt: handledAt}
|
topicRow := &t.Topic{TenantID: id, ObjHeader: t.ObjHeader{Id: topic, CreatedAt: handledAt, UpdatedAt: handledAt}, TouchedAt: handledAt}
|
||||||
if err = tenantTopicCreate(tx, topicRow); err != nil {
|
if a.tenantScopedDB {
|
||||||
|
err = a.topicCreate(tx, topicRow)
|
||||||
|
} else {
|
||||||
|
err = tenantTopicCreate(tx, topicRow)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
if !isDupe(err) {
|
if !isDupe(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1014,6 +1014,56 @@ func (messagesMapper) Edit(tenantID types.TenantID, topic string, seq int, actor
|
|||||||
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CallsPersistenceInterface interface {
|
||||||
|
Create(tenantID types.TenantID, record *types.CallRecord) error
|
||||||
|
Accept(tenantID types.TenantID, callID string, seq int, acceptedAt time.Time) error
|
||||||
|
Finish(tenantID types.TenantID, callID string, seq int, status string, durationMS int64, finishedAt time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type callsMapper struct{}
|
||||||
|
|
||||||
|
var Calls CallsPersistenceInterface
|
||||||
|
|
||||||
|
func (callsMapper) Create(tenantID types.TenantID, record *types.CallRecord) error {
|
||||||
|
if record == nil || record.TenantID != tenantID || strings.TrimSpace(record.CallID) == "" ||
|
||||||
|
record.Topic == "" || record.SeqID <= 0 || record.CallerID.IsZero() || record.CalleeID.IsZero() ||
|
||||||
|
record.CallType == "" {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantCallCreate(tenantID, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (callsMapper) Accept(tenantID types.TenantID, callID string, seq int, acceptedAt time.Time) error {
|
||||||
|
if strings.TrimSpace(callID) == "" || seq <= 0 || acceptedAt.IsZero() {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantCallAccept(tenantID, callID, seq, acceptedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (callsMapper) Finish(tenantID types.TenantID, callID string, seq int, status string, durationMS int64, finishedAt time.Time) error {
|
||||||
|
switch status {
|
||||||
|
case types.CallStatusFinished, types.CallStatusMissed, types.CallStatusDeclined, types.CallStatusDisconnected:
|
||||||
|
default:
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(callID) == "" || seq <= 0 || finishedAt.IsZero() || durationMS < 0 {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tenantAdp.TenantCallFinish(tenantID, callID, seq, status, durationMS, finishedAt)
|
||||||
|
}
|
||||||
|
|
||||||
type RelationsPersistenceInterface interface {
|
type RelationsPersistenceInterface interface {
|
||||||
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
||||||
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
||||||
@@ -2181,6 +2231,7 @@ func init() {
|
|||||||
Topics = topicsMapper{}
|
Topics = topicsMapper{}
|
||||||
Subs = subsMapper{}
|
Subs = subsMapper{}
|
||||||
Messages = messagesMapper{}
|
Messages = messagesMapper{}
|
||||||
|
Calls = callsMapper{}
|
||||||
Devices = deviceMapper{}
|
Devices = deviceMapper{}
|
||||||
Files = fileMapper{}
|
Files = fileMapper{}
|
||||||
PCache = pcacheMapper{}
|
PCache = pcacheMapper{}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
CallStatusStarted = "started"
|
||||||
|
CallStatusAccepted = "accepted"
|
||||||
|
CallStatusFinished = "finished"
|
||||||
|
CallStatusMissed = "missed"
|
||||||
|
CallStatusDeclined = "declined"
|
||||||
|
CallStatusDisconnected = "disconnected"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CallRecord struct {
|
||||||
|
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||||
|
CallID string `db:"call_id"`
|
||||||
|
Topic string
|
||||||
|
SeqID int `db:"seq_id"`
|
||||||
|
CallerID Uid `db:"caller_id"`
|
||||||
|
CalleeID Uid `db:"callee_id"`
|
||||||
|
CallType string `db:"call_type"`
|
||||||
|
Status string
|
||||||
|
EndReason string `db:"end_reason"`
|
||||||
|
DurationMS int64 `db:"duration_ms"`
|
||||||
|
StartedAt time.Time `db:"started_at"`
|
||||||
|
AcceptedAt *time.Time `db:"accepted_at"`
|
||||||
|
FinishedAt *time.Time `db:"finished_at"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
@@ -1186,6 +1186,13 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
|||||||
msg.sess.queueOut(ErrCallBusyReply(msg, types.TimeNow()))
|
msg.sess.queueOut(ErrCallBusyReply(msg, types.TimeNow()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
head, content, err := normalizeCallInvite(msg.Pub.Head, msg.Pub.Content)
|
||||||
|
if err != nil {
|
||||||
|
msg.sess.queueOut(ErrMalformedReply(msg, types.TimeNow()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg.Pub.Head = head
|
||||||
|
msg.Pub.Content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to DB at master topic.
|
// Save to DB at master topic.
|
||||||
|
|||||||
+39
-4
@@ -351,7 +351,7 @@ func TestHandleBroadcastCall(t *testing.T) {
|
|||||||
Pub: &MsgClientPub{
|
Pub: &MsgClientPub{
|
||||||
Topic: "p2p",
|
Topic: "p2p",
|
||||||
Head: map[string]any{"webrtc": "started"},
|
Head: map[string]any{"webrtc": "started"},
|
||||||
Content: "test",
|
Content: map[string]any{"callId": "call-1", "callType": "audio"},
|
||||||
NoEcho: true,
|
NoEcho: true,
|
||||||
},
|
},
|
||||||
sess: helper.sessions[0],
|
sess: helper.sessions[0],
|
||||||
@@ -383,11 +383,15 @@ func TestHandleBroadcastCall(t *testing.T) {
|
|||||||
if r.Data.Topic != from {
|
if r.Data.Topic != from {
|
||||||
t.Errorf("Response[0] topic: expected '%s', got '%s'", from, r.Data.Topic)
|
t.Errorf("Response[0] topic: expected '%s', got '%s'", from, r.Data.Topic)
|
||||||
}
|
}
|
||||||
if r.Data.Content.(string) != "test" {
|
content := r.Data.Content.(map[string]any)
|
||||||
t.Errorf("Response[0] content: expected 'test', got '%s'", r.Data.Content.(string))
|
if content["callId"] != "call-1" || content["callType"] != "audio" || content["video"] != false {
|
||||||
|
t.Errorf("Response[0] content: unexpected call payload: %#v", content)
|
||||||
}
|
}
|
||||||
if r.Data.Head == nil || r.Data.Head["webrtc"].(string) != "started" {
|
if r.Data.Head == nil || r.Data.Head["webrtc"].(string) != "started" {
|
||||||
t.Errorf("Response[0] head: expected {'webrtc': 'started'}', got '%s'", r.Data.Content.(string))
|
t.Errorf("Response[0] head: expected {'webrtc': 'started'}', got %#v", r.Data.Head)
|
||||||
|
}
|
||||||
|
if r.Data.Head["x-im-type"] != "call" || r.Data.Head["mime"] != "application/json" {
|
||||||
|
t.Errorf("Response[0] head: expected normalized call head, got %#v", r.Data.Head)
|
||||||
}
|
}
|
||||||
if r.Data.From != from {
|
if r.Data.From != from {
|
||||||
t.Errorf("Response[0] from: expected '%s', got '%s'", from, r.Data.From)
|
t.Errorf("Response[0] from: expected '%s', got '%s'", from, r.Data.From)
|
||||||
@@ -445,6 +449,37 @@ func TestHandleBroadcastCall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleBroadcastCallRequiresStructuredInvite(t *testing.T) {
|
||||||
|
numUsers := 2
|
||||||
|
helper := TopicTestHelper{}
|
||||||
|
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test" /*attach=*/, true)
|
||||||
|
globals.iceServers = []iceServer{{Username: "dummy"}}
|
||||||
|
defer func() {
|
||||||
|
globals.iceServers = nil
|
||||||
|
helper.tearDown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
from := helper.uids[0].UserId()
|
||||||
|
msg := &ClientComMessage{
|
||||||
|
AsUser: from,
|
||||||
|
Original: from,
|
||||||
|
Pub: &MsgClientPub{
|
||||||
|
Topic: "p2p",
|
||||||
|
Head: map[string]any{"webrtc": "started"},
|
||||||
|
Content: "test",
|
||||||
|
NoEcho: true,
|
||||||
|
},
|
||||||
|
sess: helper.sessions[0],
|
||||||
|
}
|
||||||
|
helper.topic.handleClientMsg(msg)
|
||||||
|
helper.finish()
|
||||||
|
|
||||||
|
verifyResponseCodes(helper.results[0], []int{http.StatusBadRequest}, t)
|
||||||
|
if len(helper.results[1].messages) != 0 {
|
||||||
|
t.Fatalf("peer: expected 0 messages, got %d", len(helper.results[1].messages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleBroadcastDataGroup(t *testing.T) {
|
func TestHandleBroadcastDataGroup(t *testing.T) {
|
||||||
topicName := "grp-test"
|
topicName := "grp-test"
|
||||||
numUsers := 4
|
numUsers := 4
|
||||||
|
|||||||
Reference in New Issue
Block a user