fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
548
consent-service/internal/services/matrix/matrix_service.go
Normal file
548
consent-service/internal/services/matrix/matrix_service.go
Normal file
@@ -0,0 +1,548 @@
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MatrixService handles Matrix homeserver communication
|
||||
type MatrixService struct {
|
||||
homeserverURL string
|
||||
accessToken string
|
||||
serverName string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Config holds Matrix service configuration
|
||||
type Config struct {
|
||||
HomeserverURL string // e.g., "http://synapse:8008"
|
||||
AccessToken string // Admin/bot access token
|
||||
ServerName string // e.g., "breakpilot.local"
|
||||
}
|
||||
|
||||
// NewMatrixService creates a new Matrix service instance
|
||||
func NewMatrixService(cfg Config) *MatrixService {
|
||||
return &MatrixService{
|
||||
homeserverURL: cfg.HomeserverURL,
|
||||
accessToken: cfg.AccessToken,
|
||||
serverName: cfg.ServerName,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Matrix API Types
|
||||
// ========================================
|
||||
|
||||
// CreateRoomRequest represents a Matrix room creation request
|
||||
type CreateRoomRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
RoomAliasName string `json:"room_alias_name,omitempty"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
Visibility string `json:"visibility,omitempty"` // "private" or "public"
|
||||
Preset string `json:"preset,omitempty"` // "private_chat", "public_chat", "trusted_private_chat"
|
||||
IsDirect bool `json:"is_direct,omitempty"`
|
||||
Invite []string `json:"invite,omitempty"`
|
||||
InitialState []StateEvent `json:"initial_state,omitempty"`
|
||||
PowerLevelContentOverride *PowerLevels `json:"power_level_content_override,omitempty"`
|
||||
}
|
||||
|
||||
// CreateRoomResponse represents a Matrix room creation response
|
||||
type CreateRoomResponse struct {
|
||||
RoomID string `json:"room_id"`
|
||||
}
|
||||
|
||||
// StateEvent represents a Matrix state event
|
||||
type StateEvent struct {
|
||||
Type string `json:"type"`
|
||||
StateKey string `json:"state_key"`
|
||||
Content interface{} `json:"content"`
|
||||
}
|
||||
|
||||
// PowerLevels represents Matrix power levels
|
||||
type PowerLevels struct {
|
||||
Ban int `json:"ban,omitempty"`
|
||||
Events map[string]int `json:"events,omitempty"`
|
||||
EventsDefault int `json:"events_default,omitempty"`
|
||||
Invite int `json:"invite,omitempty"`
|
||||
Kick int `json:"kick,omitempty"`
|
||||
Redact int `json:"redact,omitempty"`
|
||||
StateDefault int `json:"state_default,omitempty"`
|
||||
Users map[string]int `json:"users,omitempty"`
|
||||
UsersDefault int `json:"users_default,omitempty"`
|
||||
}
|
||||
|
||||
// SendMessageRequest represents a message to send
|
||||
type SendMessageRequest struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Body string `json:"body"`
|
||||
Format string `json:"format,omitempty"`
|
||||
FormattedBody string `json:"formatted_body,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo represents Matrix user information
|
||||
type UserInfo struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayName string `json:"displayname,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterRequest for user registration
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Admin bool `json:"admin,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterResponse for user registration
|
||||
type RegisterResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
|
||||
// InviteRequest for inviting a user to a room
|
||||
type InviteRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// JoinRequest for joining a room
|
||||
type JoinRequest struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Room Management
|
||||
// ========================================
|
||||
|
||||
// CreateRoom creates a new Matrix room
|
||||
func (s *MatrixService) CreateRoom(ctx context.Context, req CreateRoomRequest) (*CreateRoomResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.doRequest(ctx, "POST", "/_matrix/client/v3/createRoom", body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create room: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, s.parseError(resp)
|
||||
}
|
||||
|
||||
var result CreateRoomResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CreateClassInfoRoom creates a broadcast room for a class (teachers write, parents read)
|
||||
func (s *MatrixService) CreateClassInfoRoom(ctx context.Context, className string, schoolName string, teacherMatrixIDs []string) (*CreateRoomResponse, error) {
|
||||
// Set up power levels: teachers can write (50), parents read-only (0)
|
||||
users := make(map[string]int)
|
||||
for _, teacherID := range teacherMatrixIDs {
|
||||
users[teacherID] = 50
|
||||
}
|
||||
|
||||
req := CreateRoomRequest{
|
||||
Name: fmt.Sprintf("%s - %s (Info)", className, schoolName),
|
||||
Topic: fmt.Sprintf("Info-Kanal für %s. Nur Lehrer können schreiben.", className),
|
||||
Visibility: "private",
|
||||
Preset: "private_chat",
|
||||
Invite: teacherMatrixIDs,
|
||||
PowerLevelContentOverride: &PowerLevels{
|
||||
EventsDefault: 50, // Only power level 50+ can send messages
|
||||
UsersDefault: 0, // Parents get power level 0 by default
|
||||
Users: users,
|
||||
Invite: 50,
|
||||
Kick: 50,
|
||||
Ban: 50,
|
||||
Redact: 50,
|
||||
},
|
||||
}
|
||||
|
||||
return s.CreateRoom(ctx, req)
|
||||
}
|
||||
|
||||
// CreateStudentDMRoom creates a direct message room for parent-teacher communication about a student
|
||||
func (s *MatrixService) CreateStudentDMRoom(ctx context.Context, studentName string, className string, teacherMatrixIDs []string, parentMatrixIDs []string) (*CreateRoomResponse, error) {
|
||||
allUsers := append(teacherMatrixIDs, parentMatrixIDs...)
|
||||
|
||||
users := make(map[string]int)
|
||||
for _, id := range allUsers {
|
||||
users[id] = 50 // All can write
|
||||
}
|
||||
|
||||
req := CreateRoomRequest{
|
||||
Name: fmt.Sprintf("%s (%s) - Dialog", studentName, className),
|
||||
Topic: fmt.Sprintf("Kommunikation über %s", studentName),
|
||||
Visibility: "private",
|
||||
Preset: "trusted_private_chat",
|
||||
IsDirect: false,
|
||||
Invite: allUsers,
|
||||
InitialState: []StateEvent{
|
||||
{
|
||||
Type: "m.room.encryption",
|
||||
StateKey: "",
|
||||
Content: map[string]string{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
},
|
||||
},
|
||||
PowerLevelContentOverride: &PowerLevels{
|
||||
EventsDefault: 0, // Everyone can send messages
|
||||
UsersDefault: 50,
|
||||
Users: users,
|
||||
},
|
||||
}
|
||||
|
||||
return s.CreateRoom(ctx, req)
|
||||
}
|
||||
|
||||
// CreateParentRepRoom creates a room for class teacher and parent representatives
|
||||
func (s *MatrixService) CreateParentRepRoom(ctx context.Context, className string, teacherMatrixIDs []string, parentRepMatrixIDs []string) (*CreateRoomResponse, error) {
|
||||
allUsers := append(teacherMatrixIDs, parentRepMatrixIDs...)
|
||||
|
||||
users := make(map[string]int)
|
||||
for _, id := range allUsers {
|
||||
users[id] = 50
|
||||
}
|
||||
|
||||
req := CreateRoomRequest{
|
||||
Name: fmt.Sprintf("%s - Elternvertreter", className),
|
||||
Topic: fmt.Sprintf("Kommunikation zwischen Lehrkräften und Elternvertretern der %s", className),
|
||||
Visibility: "private",
|
||||
Preset: "private_chat",
|
||||
Invite: allUsers,
|
||||
PowerLevelContentOverride: &PowerLevels{
|
||||
EventsDefault: 0,
|
||||
UsersDefault: 50,
|
||||
Users: users,
|
||||
},
|
||||
}
|
||||
|
||||
return s.CreateRoom(ctx, req)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// User Management
|
||||
// ========================================
|
||||
|
||||
// RegisterUser registers a new Matrix user (requires admin token)
|
||||
func (s *MatrixService) RegisterUser(ctx context.Context, username string, displayName string) (*RegisterResponse, error) {
|
||||
// Use admin API for user registration
|
||||
req := map[string]interface{}{
|
||||
"username": username,
|
||||
"password": uuid.New().String(), // Generate random password
|
||||
"admin": false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// Use admin registration endpoint
|
||||
resp, err := s.doRequest(ctx, "POST", "/_synapse/admin/v2/users/@"+username+":"+s.serverName, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register user: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return nil, s.parseError(resp)
|
||||
}
|
||||
|
||||
// Set display name
|
||||
if displayName != "" {
|
||||
if err := s.SetDisplayName(ctx, "@"+username+":"+s.serverName, displayName); err != nil {
|
||||
// Log but don't fail
|
||||
fmt.Printf("Warning: failed to set display name: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &RegisterResponse{
|
||||
UserID: "@" + username + ":" + s.serverName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetDisplayName sets the display name for a user
|
||||
func (s *MatrixService) SetDisplayName(ctx context.Context, userID string, displayName string) error {
|
||||
req := map[string]string{
|
||||
"displayname": displayName,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/_matrix/client/v3/profile/%s/displayname", url.PathEscape(userID))
|
||||
resp, err := s.doRequest(ctx, "PUT", endpoint, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set display name: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Room Membership
|
||||
// ========================================
|
||||
|
||||
// InviteUser invites a user to a room
|
||||
func (s *MatrixService) InviteUser(ctx context.Context, roomID string, userID string) error {
|
||||
req := InviteRequest{UserID: userID}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/_matrix/client/v3/rooms/%s/invite", url.PathEscape(roomID))
|
||||
resp, err := s.doRequest(ctx, "POST", endpoint, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to invite user: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JoinRoom makes the bot join a room
|
||||
func (s *MatrixService) JoinRoom(ctx context.Context, roomIDOrAlias string) error {
|
||||
endpoint := fmt.Sprintf("/_matrix/client/v3/join/%s", url.PathEscape(roomIDOrAlias))
|
||||
resp, err := s.doRequest(ctx, "POST", endpoint, []byte("{}"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to join room: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUserPowerLevel sets a user's power level in a room
|
||||
func (s *MatrixService) SetUserPowerLevel(ctx context.Context, roomID string, userID string, powerLevel int) error {
|
||||
// First, get current power levels
|
||||
endpoint := fmt.Sprintf("/_matrix/client/v3/rooms/%s/state/m.room.power_levels/", url.PathEscape(roomID))
|
||||
resp, err := s.doRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get power levels: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp)
|
||||
}
|
||||
|
||||
var powerLevels PowerLevels
|
||||
if err := json.NewDecoder(resp.Body).Decode(&powerLevels); err != nil {
|
||||
return fmt.Errorf("failed to decode power levels: %w", err)
|
||||
}
|
||||
|
||||
// Update user power level
|
||||
if powerLevels.Users == nil {
|
||||
powerLevels.Users = make(map[string]int)
|
||||
}
|
||||
powerLevels.Users[userID] = powerLevel
|
||||
|
||||
// Send updated power levels
|
||||
body, err := json.Marshal(powerLevels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal power levels: %w", err)
|
||||
}
|
||||
|
||||
resp2, err := s.doRequest(ctx, "PUT", endpoint, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set power levels: %w", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp2)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Messaging
|
||||
// ========================================
|
||||
|
||||
// SendMessage sends a text message to a room
|
||||
func (s *MatrixService) SendMessage(ctx context.Context, roomID string, message string) error {
|
||||
req := SendMessageRequest{
|
||||
MsgType: "m.text",
|
||||
Body: message,
|
||||
}
|
||||
|
||||
return s.sendEvent(ctx, roomID, "m.room.message", req)
|
||||
}
|
||||
|
||||
// SendHTMLMessage sends an HTML-formatted message to a room
|
||||
func (s *MatrixService) SendHTMLMessage(ctx context.Context, roomID string, plainText string, htmlBody string) error {
|
||||
req := SendMessageRequest{
|
||||
MsgType: "m.text",
|
||||
Body: plainText,
|
||||
Format: "org.matrix.custom.html",
|
||||
FormattedBody: htmlBody,
|
||||
}
|
||||
|
||||
return s.sendEvent(ctx, roomID, "m.room.message", req)
|
||||
}
|
||||
|
||||
// SendAbsenceNotification sends an absence notification to parents
|
||||
func (s *MatrixService) SendAbsenceNotification(ctx context.Context, roomID string, studentName string, date string, lessonNumber int) error {
|
||||
plainText := fmt.Sprintf("⚠️ Abwesenheitsmeldung\n\nIhr Kind %s war heute (%s) in der %d. Stunde nicht im Unterricht anwesend.\n\nBitte bestätigen Sie den Grund der Abwesenheit.", studentName, date, lessonNumber)
|
||||
|
||||
htmlBody := fmt.Sprintf(`<h3>⚠️ Abwesenheitsmeldung</h3>
|
||||
<p>Ihr Kind <strong>%s</strong> war heute (%s) in der <strong>%d. Stunde</strong> nicht im Unterricht anwesend.</p>
|
||||
<p>Bitte bestätigen Sie den Grund der Abwesenheit.</p>
|
||||
<ul>
|
||||
<li>✅ Entschuldigt (Krankheit)</li>
|
||||
<li>📋 Arztbesuch</li>
|
||||
<li>❓ Sonstiges (bitte erläutern)</li>
|
||||
</ul>`, studentName, date, lessonNumber)
|
||||
|
||||
return s.SendHTMLMessage(ctx, roomID, plainText, htmlBody)
|
||||
}
|
||||
|
||||
// SendGradeNotification sends a grade notification to parents
|
||||
func (s *MatrixService) SendGradeNotification(ctx context.Context, roomID string, studentName string, subject string, gradeType string, grade float64) error {
|
||||
plainText := fmt.Sprintf("📊 Neue Note eingetragen\n\nFür %s wurde eine neue Note eingetragen:\n\nFach: %s\nArt: %s\nNote: %.1f", studentName, subject, gradeType, grade)
|
||||
|
||||
htmlBody := fmt.Sprintf(`<h3>📊 Neue Note eingetragen</h3>
|
||||
<p>Für <strong>%s</strong> wurde eine neue Note eingetragen:</p>
|
||||
<table>
|
||||
<tr><td>Fach:</td><td><strong>%s</strong></td></tr>
|
||||
<tr><td>Art:</td><td>%s</td></tr>
|
||||
<tr><td>Note:</td><td><strong>%.1f</strong></td></tr>
|
||||
</table>`, studentName, subject, gradeType, grade)
|
||||
|
||||
return s.SendHTMLMessage(ctx, roomID, plainText, htmlBody)
|
||||
}
|
||||
|
||||
// SendClassAnnouncement sends an announcement to a class info room
|
||||
func (s *MatrixService) SendClassAnnouncement(ctx context.Context, roomID string, title string, content string, teacherName string) error {
|
||||
plainText := fmt.Sprintf("📢 %s\n\n%s\n\n— %s", title, content, teacherName)
|
||||
|
||||
htmlBody := fmt.Sprintf(`<h3>📢 %s</h3>
|
||||
<p>%s</p>
|
||||
<p><em>— %s</em></p>`, title, content, teacherName)
|
||||
|
||||
return s.SendHTMLMessage(ctx, roomID, plainText, htmlBody)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Internal Helpers
|
||||
// ========================================
|
||||
|
||||
func (s *MatrixService) sendEvent(ctx context.Context, roomID string, eventType string, content interface{}) error {
|
||||
body, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal content: %w", err)
|
||||
}
|
||||
|
||||
txnID := uuid.New().String()
|
||||
endpoint := fmt.Sprintf("/_matrix/client/v3/rooms/%s/send/%s/%s",
|
||||
url.PathEscape(roomID), url.PathEscape(eventType), txnID)
|
||||
|
||||
resp, err := s.doRequest(ctx, "PUT", endpoint, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send event: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return s.parseError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MatrixService) doRequest(ctx context.Context, method string, endpoint string, body []byte) (*http.Response, error) {
|
||||
fullURL := s.homeserverURL + endpoint
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
bodyReader = bytes.NewReader(body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+s.accessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return s.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (s *MatrixService) parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var errResp struct {
|
||||
ErrCode string `json:"errcode"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &errResp); err != nil {
|
||||
return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return fmt.Errorf("matrix error %s: %s", errResp.ErrCode, errResp.Error)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Health Check
|
||||
// ========================================
|
||||
|
||||
// HealthCheck checks if the Matrix server is reachable
|
||||
func (s *MatrixService) HealthCheck(ctx context.Context) error {
|
||||
resp, err := s.doRequest(ctx, "GET", "/_matrix/client/versions", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("matrix server unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("matrix server returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServerName returns the configured server name
|
||||
func (s *MatrixService) GetServerName() string {
|
||||
return s.serverName
|
||||
}
|
||||
|
||||
// GenerateUserID generates a Matrix user ID from a username
|
||||
func (s *MatrixService) GenerateUserID(username string) string {
|
||||
return fmt.Sprintf("@%s:%s", username, s.serverName)
|
||||
}
|
||||
Reference in New Issue
Block a user