feat: switch client API branding to MeetingIM

This commit is contained in:
richjohnrambo
2026-08-31 01:22:53 +08:00
parent c6a243ee59
commit 9f647de4d0
48 changed files with 717 additions and 271 deletions
+35
View File
@@ -275,6 +275,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
if err = ensureIMFileReviewTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure file review schema: " + err.Error())
}
if err = ensureIMOrganizationVisibilityTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure organization visibility schema: " + err.Error())
}
if err = ensureSnowflakePrimaryKeys(a.db); err != nil {
return errors.New("mysql adapter failed to ensure snowflake primary keys: " + err.Error())
}
@@ -507,6 +510,10 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
db.Close()
return nil, nil, err
}
if err = ensureIMOrganizationVisibilityTables(db); err != nil {
db.Close()
return nil, nil, err
}
if err = ensureSnowflakePrimaryKeys(db); err != nil {
db.Close()
return nil, nil, err
@@ -1187,6 +1194,18 @@ func createIMOrganizationTables(tx *sql.Tx) error {
INDEX idx_im_org_members_display_name(display_name),
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES im_org_departments(department_id) ON DELETE CASCADE
)`,
`CREATE TABLE im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
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_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`,
}
for _, stmt := range statements {
if _, err := tx.Exec(stmt); err != nil {
@@ -1196,6 +1215,22 @@ func createIMOrganizationTables(tx *sql.Tx) error {
return nil
}
func ensureIMOrganizationVisibilityTables(exec sqlExecutor) error {
_, err := exec.Exec(`CREATE TABLE IF NOT EXISTS im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
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_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`)
return err
}
func createIMMeetingTables(tx *sql.Tx) error {
statements := []string{
`CREATE TABLE im_meetings(
+174 -24
View File
@@ -2681,12 +2681,28 @@ func (a *adapter) TenantOrganizationDepartments(id t.TenantID, uid t.Uid, parent
if cancel != nil {
defer cancel()
}
rows, err := a.db.QueryxContext(ctx, `SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
cte, cteArgs := visibility.departmentCTE()
memberCountSQL, memberCountArgs := visibility.memberCountSQL("m")
if visibility.Scope != orgVisibilityAll {
where = append(where, "d.department_id IN (SELECT department_id FROM visible_depts)")
}
queryArgs := append([]any{}, cteArgs...)
queryArgs = append(queryArgs, memberCountArgs...)
queryArgs = append(queryArgs, args...)
childCountSQL := `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active')`
if visibility.Scope != orgVisibilityAll {
childCountSQL = `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active' AND child.department_id IN (SELECT department_id FROM visible_depts))`
}
rows, err := a.db.QueryxContext(ctx, cte+`SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
`+childCountSQL+` AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id AND m.employment_state='active'`+memberCountSQL+`) AS member_count
FROM im_org_departments AS d
WHERE `+strings.Join(where, " AND ")+`
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, args...)
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, queryArgs...)
if err != nil {
return nil, "", err
}
@@ -2726,7 +2742,7 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
if limit <= 0 || limit > 100 {
limit = 50
}
where := []string{"m.department_id=?"}
where := []string{"m.department_id=?", "m.employment_state='active'"}
args := []any{strings.TrimSpace(departmentID)}
if cursor != "" {
parsed, err := strconv.ParseInt(cursor, 10, 64)
@@ -2741,6 +2757,15 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
if cancel != nil {
defer cancel()
}
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
where = append(where, visibilitySQL)
args = append(args, visibilityArgs...)
}
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
FROM im_org_members AS m
@@ -2809,6 +2834,15 @@ func (a *adapter) TenantOrganizationMembersAll(id t.TenantID, uid t.Uid, departm
if cancel != nil {
defer cancel()
}
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
where = append(where, visibilitySQL)
args = append(args, visibilityArgs...)
}
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
FROM im_org_members AS m
@@ -2866,29 +2900,45 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
return nil, "", t.ErrMalformed
}
like := "%" + strings.TrimSpace(queryText) + "%"
parts := make([]string, 0, 2)
args := make([]any, 0, 6)
if includeDepartments {
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
FROM im_org_departments AS d WHERE d.state='active' AND d.name LIKE ?`)
args = append(args, like)
}
if includeUsers {
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
FROM im_org_members AS m
WHERE m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?`)
args = append(args, like, like, like, like)
}
args = append(args, offset, limit+1)
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
cte, cteArgs := visibility.departmentCTE()
parts := make([]string, 0, 2)
args := make([]any, 0, 10)
args = append(args, cteArgs...)
if includeDepartments {
departmentWhere := "d.state='active' AND d.name LIKE ?"
if visibility.Scope != orgVisibilityAll {
departmentWhere += " AND d.department_id IN (SELECT department_id FROM visible_depts)"
}
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
FROM im_org_departments AS d WHERE `+departmentWhere)
args = append(args, like)
}
if includeUsers {
userWhere := "(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?) AND m.employment_state='active'"
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
userWhere += " AND " + visibilitySQL
}
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
FROM im_org_members AS m
WHERE `+userWhere)
args = append(args, like, like, like, like)
args = append(args, visibilityArgs...)
}
args = append(args, offset, limit+1)
rows, err := a.db.QueryxContext(ctx, cte+`SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
ORDER BY result_type ASC,id ASC LIMIT ?,?`, args...)
if err != nil {
return nil, "", err
@@ -2913,6 +2963,106 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
return items, nextCursor, nil
}
const (
orgVisibilityAll = "all"
orgVisibilityFriends = "friends"
orgVisibilitySelf = "self"
)
type organizationVisibility struct {
Scope string
UserID int64
}
func (a *adapter) organizationVisibility(ctx context.Context, uid t.Uid) (organizationVisibility, error) {
userID := decodeUID(uid)
visibility := organizationVisibility{Scope: orgVisibilitySelf, UserID: userID}
if userID <= 0 {
return visibility, t.ErrMalformed
}
var override string
err := a.db.QueryRowxContext(ctx, `SELECT visibility_scope FROM im_org_visibility_overrides
WHERE user_id=? AND state='active' LIMIT 1`, userID).Scan(&override)
if err != nil && err != sql.ErrNoRows {
return visibility, err
}
if err == nil {
switch strings.TrimSpace(override) {
case orgVisibilityAll, orgVisibilityFriends, orgVisibilitySelf:
visibility.Scope = strings.TrimSpace(override)
return visibility, nil
default:
return visibility, t.ErrMalformed
}
}
var phone sql.NullString
err = a.db.QueryRowxContext(ctx, `SELECT phone FROM im_users WHERE id=? AND state<>? LIMIT 1`, userID, t.StateDeleted).Scan(&phone)
if err == sql.ErrNoRows {
return visibility, t.ErrNotFound
}
if err != nil {
return visibility, err
}
var inviteUsed int
err = a.db.QueryRowxContext(ctx, `SELECT EXISTS(SELECT 1 FROM im_registration_invite_uses WHERE user_id=?)`, userID).Scan(&inviteUsed)
if err != nil {
return visibility, err
}
if inviteUsed > 0 {
visibility.Scope = orgVisibilitySelf
return visibility, nil
}
if phone.Valid && strings.TrimSpace(phone.String) != "" {
visibility.Scope = orgVisibilityFriends
}
return visibility, nil
}
func (v organizationVisibility) memberWhereSQL(alias string) (string, []any) {
alias = strings.TrimSpace(alias)
if alias == "" {
alias = "m"
}
switch v.Scope {
case orgVisibilityAll:
return "", nil
case orgVisibilityFriends:
return "(" + alias + ".user_id=? OR EXISTS (SELECT 1 FROM im_friendships AS fs WHERE fs.user_id=? AND fs.peer_id=" + alias + ".user_id AND fs.status=?))",
[]any{v.UserID, v.UserID, t.RelationshipFriend}
default:
return alias + ".user_id=?", []any{v.UserID}
}
}
func (v organizationVisibility) memberCountSQL(alias string) (string, []any) {
where, args := v.memberWhereSQL(alias)
if where == "" {
return "", nil
}
return " AND " + where, args
}
func (v organizationVisibility) departmentCTE() (string, []any) {
if v.Scope == orgVisibilityAll {
return "", nil
}
memberWhere, args := v.memberWhereSQL("m")
return `WITH RECURSIVE visible_member_depts AS (
SELECT DISTINCT m.department_id FROM im_org_members AS m
WHERE m.employment_state='active' AND ` + memberWhere + `
), visible_depts AS (
SELECT d.department_id,d.parent_id FROM im_org_departments AS d
JOIN visible_member_depts AS vmd ON vmd.department_id=d.department_id
WHERE d.state='active'
UNION DISTINCT
SELECT parent.department_id,parent.parent_id FROM im_org_departments AS parent
JOIN visible_depts AS child ON child.parent_id=parent.department_id
WHERE parent.state='active'
) `, args
}
func organizationSearchIncludes(resultTypes string) (bool, bool) {
if strings.TrimSpace(resultTypes) == "" {
return true, true
+3 -3
View File
@@ -300,7 +300,7 @@ func cacheControlHandler(maxAge int, handler http.Handler) http.Handler {
// Get API key from an HTTP request.
func getAPIKey(req *http.Request) string {
// Check header.
apikey := req.Header.Get("X-Tinode-APIKey")
apikey := req.Header.Get("X-MeetingIM-APIKey")
if apikey != "" {
return apikey
}
@@ -328,8 +328,8 @@ func getAPIKey(req *http.Request) string {
// Extracts authorization credentials from an HTTP request.
// Returns authentication method and secret.
func getHttpAuth(req *http.Request) (method, secret string) {
// Check X-Tinode-Auth header.
if parts := strings.Split(req.Header.Get("X-Tinode-Auth"), " "); len(parts) == 2 {
// Check MeetingIM product auth header.
if parts := strings.Split(req.Header.Get("X-MeetingIM-Auth"), " "); len(parts) == 2 {
method, secret = parts[0], parts[1]
return
}
+18 -18
View File
@@ -110,7 +110,7 @@ func withIMHTTPProtocol(prefix string, next http.Handler) http.Handler {
wrt.Header().Set("X-Request-ID", requestID)
wrt.Header().Set("X-IM-Schema", "1")
if valid, _ := checkAPIKey(strings.TrimSpace(req.Header.Get("X-Tinode-APIKey"))); !valid {
if valid, _ := checkAPIKey(strings.TrimSpace(getAPIKey(req))); !valid {
writeIMError(wrt, req, http.StatusForbidden, 30003, imReasonAPIKeyRequired, "valid API key required", nil)
return
}
@@ -7283,24 +7283,24 @@ func meetingPayloadWithUserSummaries(meeting *types.Meeting, viewer types.Uid, u
startAt := meeting.StartAt.UTC()
endAt := meeting.EndAt.UTC()
data := map[string]any{
"meeting_id": meeting.MeetingID,
"title": meeting.Title,
"organizer_id": meeting.OrganizerID.UserId(),
"organizer": organizer,
"start_at": startAt.Format(time.RFC3339Nano),
"end_at": endAt.Format(time.RFC3339Nano),
"meeting_time": meetingTimeRange(startAt, endAt),
"time_range": meetingTimeRange(startAt, endAt),
"location": meeting.Location,
"state": meeting.State,
"reminder_minutes": meeting.ReminderMinutes,
"version": meeting.Version,
"participants": participants,
"meeting_id": meeting.MeetingID,
"title": meeting.Title,
"organizer_id": meeting.OrganizerID.UserId(),
"organizer": organizer,
"start_at": startAt.Format(time.RFC3339Nano),
"end_at": endAt.Format(time.RFC3339Nano),
"meeting_time": meetingTimeRange(startAt, endAt),
"time_range": meetingTimeRange(startAt, endAt),
"location": meeting.Location,
"state": meeting.State,
"reminder_minutes": meeting.ReminderMinutes,
"version": meeting.Version,
"participants": participants,
"participant_count": len(participants),
"participant_names": meetingParticipantNames(participants),
"permissions": meetingPermissionsPayload(meeting, viewer),
"created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano),
"updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano),
"permissions": meetingPermissionsPayload(meeting, viewer),
"created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano),
"updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano),
}
if displayName, ok := organizer["display_name"]; ok {
data["organizer_display_name"] = displayName
@@ -7681,7 +7681,7 @@ func imCanViewLastSeen(tenantID types.TenantID, viewer, target types.Uid) bool {
if viewer == target {
return true
}
// Legacy deployments without the product privacy stores retain Tinode's
// Legacy deployments without the product privacy stores retain MeetingIM's
// original presence behavior.
if store.Profiles == nil || store.Relations == nil {
return true
+41 -18
View File
@@ -1226,7 +1226,7 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
}
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
req.Header.Set("X-Tinode-APIKey", apiKey)
req.Header.Set("X-MeetingIM-APIKey", apiKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest || decodeIMHTTPResponse(t, rec).Reason != imReasonSchemaUnsupported {
@@ -1234,7 +1234,7 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
}
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
req.Header.Set("X-Tinode-APIKey", apiKey)
req.Header.Set("X-MeetingIM-APIKey", apiKey)
req.Header.Set("X-IM-Schema", "1")
req.Header.Set("X-Request-ID", "request-1")
rec = httptest.NewRecorder()
@@ -1242,6 +1242,29 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
if rec.Code != http.StatusOK || called != 1 || rec.Header().Get("X-Request-ID") != "request-1" {
t.Fatalf("valid protocol request: status=%d called=%d request_id=%q", rec.Code, called, rec.Header().Get("X-Request-ID"))
}
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
req.Header.Set("X-MeetingIM-APIKey", apiKey)
req.Header.Set("X-IM-Schema", "1")
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || called != 2 {
t.Fatalf("legacy protocol request: status=%d called=%d", rec.Code, called)
}
}
func TestMeetingIMHTTPAuthHeaders(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v0/file/s/file1", nil)
req.Header.Set("X-MeetingIM-APIKey", "new-api-key")
req.Header.Set("X-MeetingIM-Auth", "token new-token")
if got := getAPIKey(req); got != "new-api-key" {
t.Fatalf("getAPIKey new header=%q", got)
}
method, secret := getHttpAuth(req)
if method != "token" || secret != "new-token" {
t.Fatalf("getHttpAuth new header method=%q secret=%q", method, secret)
}
}
func TestIMHTTPIdempotencyReplayAndConflict(t *testing.T) {
@@ -1263,7 +1286,7 @@ func TestIMHTTPIdempotencyReplayAndConflict(t *testing.T) {
request := func(body string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, "/v1/im/test", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer token-1")
req.Header.Set("X-Tinode-APIKey", apiKey)
req.Header.Set("X-MeetingIM-APIKey", apiKey)
req.Header.Set("X-IM-Schema", "1")
req.Header.Set("Idempotency-Key", "same-key")
rec := httptest.NewRecorder()
@@ -1298,7 +1321,7 @@ func TestServeIMTenantResolveSuccessFromHeader(t *testing.T) {
globals.servingAt = "https://im.example.com/"
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve", nil)
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", " acme ")
req.Header.Set("X-Request-ID", "req-1")
rec := httptest.NewRecorder()
@@ -1331,7 +1354,7 @@ func TestIMHTTPProtocolDoesNotRequireIdempotencyForTenantResolve(t *testing.T) {
writeIMSuccess(w, req, map[string]any{"ok": true})
})
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve", nil)
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-IM-Schema", "1")
req.Header.Set("Authorization", "Bearer test-token")
rec := httptest.NewRecorder()
@@ -1362,7 +1385,7 @@ func TestServeIMTenantResolveUsesForwardedPublicHost(t *testing.T) {
req.Host = "internal:6060"
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "api.118im.com")
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1393,7 +1416,7 @@ func TestServeIMTenantResolveSuccessFromBody(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
bytes.NewBufferString(`{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
rec := httptest.NewRecorder()
serveIMTenantResolve(rec, req)
@@ -1441,7 +1464,7 @@ func TestServeIMTenantResolveRequiresAPIKey(t *testing.T) {
func TestServeIMTenantResolveRejectsMalformedTenantCode(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
bytes.NewBufferString(`{"tenant_code":"`+strings.Repeat("a", 65)+`"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
rec := httptest.NewRecorder()
serveIMTenantResolve(rec, req)
@@ -1467,7 +1490,7 @@ func TestServeIMTenantResolveHidesUnavailableTenantState(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
bytes.NewBufferString(`{"tenant_code":"acme"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
rec := httptest.NewRecorder()
serveIMTenantResolve(rec, req)
@@ -1488,7 +1511,7 @@ func TestServeIMTenantResolveHidesMissingTenant(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
bytes.NewBufferString(`{"tenant_code":"acme"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
rec := httptest.NewRecorder()
serveIMTenantResolve(rec, req)
@@ -1639,7 +1662,7 @@ func TestServeIMVerificationCodesCreatesChallenge(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1689,7 +1712,7 @@ func TestServeIMVerificationCodesSkipsDeliveryWhenRegistrationSMSDisabled(t *tes
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1729,7 +1752,7 @@ func TestServeIMPasswordResetCodeFindsPhoneWithoutTelCredential(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"reset_password","phone":"13800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1768,7 +1791,7 @@ func TestServeIMVerificationCodesKeepsChallengeWhenDeliveryUnavailable(t *testin
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"reset_password","phone":"+8613800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1786,7 +1809,7 @@ func TestServeIMVerificationCodesKeepsChallengeWhenDeliveryUnavailable(t *testin
func TestServeIMVerificationCodesRequiresTenantCode(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
rec := httptest.NewRecorder()
serveIMVerificationCodes(rec, req)
@@ -1825,7 +1848,7 @@ func TestServeIMVerificationCodesRateLimited(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1868,7 +1891,7 @@ func TestServeIMVerificationCodeVerifySuccess(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes/challenge-1/verify",
bytes.NewBufferString(`{"code":"123456"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -1913,7 +1936,7 @@ func TestServeIMVerificationCodeVerifyInvalidCode(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes/challenge-1/verify",
bytes.NewBufferString(`{"code":"654321"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
+1 -1
View File
@@ -341,7 +341,7 @@ func main() {
logFlags := flag.String("log_flags", "stdFlags",
"Comma-separated list of log flags (as defined in https://golang.org/pkg/log/#pkg-constants without the L prefix)")
configfile := flag.String("config", "tinode.conf", "Path to config file.")
configfile := flag.String("config", "meetingim.conf", "Path to config file.")
// Path to static content.
staticPath := flag.String("static_data", defaultStaticPath, "File path to directory with static files to be served.")
listenOn := flag.String("listen", "", "Override address and port to listen on for HTTP(S) clients.")
+223
View File
@@ -0,0 +1,223 @@
{
"listen": ":6060",
"api_path": "/",
"cache_control": 39600,
"ws_compression_disabled": false,
"static_mount": "/",
"static_data": "static",
"grpc_listen": ":16060",
"grpc_keepalive_enabled": true,
"api_key_salt": "T713/rYYgW7g4m3vG6zGRh7+FM1t0T8j13koXScOAj4=",
"max_message_size": 131072,
"max_subscriber_count": 128,
"max_tag_count": 16,
"permanent_accounts": false,
"expvar": "/debug/vars",
"server_status": "-",
"use_x_forwarded_for": true,
"x_frame_options": "SAMEORIGIN",
"default_country_code": "CN",
"p2p_delete_enabled": true,
"msg_delete_age": 600,
"alias_tag": "alias",
"media": {
"use_handler": "fs",
"max_size": 104857600,
"gc_period": 3600,
"gc_block_size": 100,
"handlers": {
"fs": {
"upload_dir": "uploads",
"cache_control": "max-age=86400",
"cors_origins": ["*"]
}
}
},
"webrtc": {
"enabled": true,
"call_establishment_timeout": 30,
"ice_servers": [
{
"urls": ["stun:stun.l.google.com:19302"]
}
]
},
"tls": {
"enabled": false,
"http_redirect": "",
"strict_max_age": 0
},
"auth_config": {
"logical_names": [],
"basic": {
"add_to_tags": false,
"min_login_length": 1,
"min_password_length": 6
},
"token": {
"expire_in": 1209600,
"serial_num": 1,
"key": "wfaY2RgF2S1OQI/ZlK+LSrp1KB2jwAdGAIHQ7JZn+Kc="
},
"code": {
"expire_in": 900,
"max_retries": 3,
"code_length": 6
}
},
"store_config": {
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
"max_results": 1024,
"use_adapter": "mysql",
"adapters": {
"mysql": {
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(64.118.137.182:53306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci",
"tenant_registry_db": "hotgo",
"tenant_db_routing": true,
"max_open_conns": 64,
"max_idle_conns": 64,
"conn_max_lifetime": 60,
"sql_timeout": 10
}
},
"redis": {
"addr": "64.118.137.182:56379",
"username": "",
"password": "HMTJzkm.h>Y+E9ep",
"db": 0,
"key_prefix": "im",
"dial_timeout_ms": 2000,
"read_timeout_ms": 2000,
"write_timeout_ms": 2000,
"pending_ttl_seconds": 120,
"done_ttl_seconds": 86400,
"failed_ttl_seconds": 600,
"hot_message_ttl_seconds": 86400,
"presence_ttl_seconds": 120,
"cache_ttl_seconds": 300,
"short_state_ttl_seconds": 600,
"group_fanout_enabled": true
}
},
"kafka": {
"enabled": true,
"brokers": ["64.118.137.182:9094"],
"client_id": "im-server",
"consumer_group": "im-server",
"dial_timeout_ms": 3000,
"write_timeout_ms": 3000,
"read_timeout_ms": 3000,
"required_acks": 1,
"allow_auto_topic_creation": false,
"tls_enabled": false,
"sasl_enabled": true,
"sasl_mechanism": "SCRAM-SHA-512",
"sasl_username": "kafka-external",
"sasl_password": "gr58tNnlZOVGW3AmMbL2",
"max_retry_count": 5,
"retry_backoff_ms": [10000, 30000, 60000, 300000, 900000],
"dead_letter_retention_seconds": 7776000,
"topics": {
"admin_command": "im.admin.command",
"admin_command_result": "im.admin.command.result",
"config_event": "im.config.event",
"business_event": "im.business.event",
"message_persist": "im.message.persist",
"message_push": "im.message.push",
"file_security": "im.file.security",
"file_lifecycle": "im.file.lifecycle",
"workbench_event": "im.workbench.event",
"cache_invalidate": "im.cache.invalidate",
"audit_event": "im.audit.event",
"dead_letter": "im.dead.letter"
}
},
"internal_api": {
"enabled": true,
"time_window_seconds": 300,
"services": {
"hotgo-admin": {
"secret": "8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b",
"allow": ["admin_query"]
}
}
},
"admin_internal_api": {
"enabled": true,
"base_url": "http://im-admin:8000",
"service_name": "im-server",
"secret": "b96832c04dbfb6dd768930ec9f082ecd90d87a1ffdaa493aa070617c48de9097",
"timeout_ms": 3000
},
"acc_validation": {
"email": {
"add_to_tags": false,
"required": [],
"config": {
"host_url": "http://64.118.137.182:6060/",
"sender": "MeetingIM <noreply@118im.com>",
"validation_templ": "server/templ/email-validation-en.templ",
"reset_secret_templ": "server/templ/email-password-reset-en.templ",
"debug_response": "123456",
"max_retries": 3
}
},
"tel": {
"add_to_tags": false,
"required": [],
"config": {
"host_url": "http://64.118.137.182:6060/",
"sender": "MeetingIM",
"universal_templ": "server/templ/sms-universal-en.templ",
"debug_response": "123456",
"max_retries": 3
}
}
},
"acc_gc_config": {
"enabled": true,
"gc_period": 3600,
"gc_block_size": 10,
"gc_min_account_age": 30
},
"push": [
{
"name": "stdout",
"config": {
"enabled": false
}
},
{
"name": "fcm",
"config": {
"enabled": false
}
},
{
"name": "tnpg",
"config": {
"enabled": false
}
}
],
"cluster_config": {
"self": "",
"nodes": [],
"failover": {
"enabled": false
}
},
"plugins": []
}
+7 -7
View File
@@ -9,7 +9,7 @@ HTTP_BASE_PORT=6060
# Port where the first node will listen for gRPC intra-cluster connections.
GRPC_BASE_PORT=16060
USAGE="Usage: $0 [ --config <path_to_tinode.conf> ] {start|stop}"
USAGE="Usage: $0 [ --config <path_to_meetingim.conf> ] {start|stop}"
# Your server binary may have a different name and location.
SERVER='./server'
@@ -34,9 +34,9 @@ while [[ $# -gt 0 ]]; do
;;
start)
if [ ! -z "$config" ] ; then
TINODE_CONF=$config
MEETINGIM_CONF=$config
else
TINODE_CONF="tinode.conf"
MEETINGIM_CONF="meetingim.conf"
fi
if [ ! -z "${static_data+x}" ] ; then
STATIC_DATA_DIR=$static_data
@@ -51,10 +51,10 @@ while [[ $# -gt 0 ]]; do
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
do
# Start the node
$SERVER -config=${TINODE_CONF} -cluster_self=${NODE_NAME} -listen=:${HTTP_PORT} -grpc_listen=:${GRPC_PORT} -static_data=${STATIC_DATA_DIR} -log_flags=stdFlags,shortfile &
$SERVER -config=${MEETINGIM_CONF} -cluster_self=${NODE_NAME} -listen=:${HTTP_PORT} -grpc_listen=:${GRPC_PORT} -static_data=${STATIC_DATA_DIR} -log_flags=stdFlags,shortfile &
# Save PID of the node to a temp file.
# /var/tmp/ does not requre root access.
echo $!> "/var/tmp/tinode-${NODE_NAME}.pid"
echo $!> "/var/tmp/meetingim-${NODE_NAME}.pid"
# Increment ports for the next node.
HTTP_PORT=$((HTTP_PORT+1))
GRPC_PORT=$((GRPC_PORT+1))
@@ -67,9 +67,9 @@ while [[ $# -gt 0 ]]; do
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
do
# Read PIDs of running nodes from temp files and kill them.
kill `cat /var/tmp/tinode-${NODE_NAME}.pid`
kill `cat /var/tmp/meetingim-${NODE_NAME}.pid`
# Clean up: delete temp files.
rm "/var/tmp/tinode-${NODE_NAME}.pid"
rm "/var/tmp/meetingim-${NODE_NAME}.pid"
done
exit 0
;;
+12 -12
View File
@@ -1,7 +1,7 @@
#!/bin/bash
BINARY_PATH=$GOPATH/bin
TINODE_BINARY=$BINARY_PATH/server
MEETINGIM_BINARY=$BINARY_PATH/server
# Kills and removes any running containers.
cleanup() {
@@ -45,23 +45,23 @@ setup() {
echo -n "."
sleep 1
done
# Make sure there's no Tinode server binary in the current directory.
# Make sure there's no MeetingIM server binary in the current directory.
if [ -f "./server" ]; then
rm ./server
fi
}
# Compiles Tinode binaries.
# Compiles MeetingIM binaries.
build() {
go install -tags mysql -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`" \
github.com/tinode/chat/tinode-db \
github.com/tinode/chat/server && \
ln -s $TINODE_BINARY
ln -s $MEETINGIM_BINARY
}
# Initializes Tinode database.
# Initializes MeetingIM database.
init-db() {
$GOPATH/bin/tinode-db -config=./tinode.conf -data=../tinode-db/data.json
$GOPATH/bin/tinode-db -config=./meetingim.conf -data=../tinode-db/data.json
}
wait-for() {
@@ -71,7 +71,7 @@ wait-for() {
done
}
# Brings up a three-node Tinode cluster.
# Brings up a three-node MeetingIM cluster.
run-server() {
./run-cluster.sh -s "" start && wait-for 16060
}
@@ -80,7 +80,7 @@ send-requests() {
local expect=12
local port=$1
local id=$2
local outfile=$(mktemp /tmp/tinode-${id}.txt)
local outfile=$(mktemp /tmp/meetingim-${id}.txt)
pushd .
cd ../tn-cli
python3 tn-cli.py --host=localhost:${port} --no-login < sample-script.txt > $outfile || fail "Test script failed (instance port ${port})"
@@ -103,14 +103,14 @@ trap 'cleanup ; fail "For Unexpected Reasons"'\
run_id=`date +%s`
echo "+----------------------------------------------------+"
echo "| Tinode sanity test. |"
echo "| MeetingIM sanity test. |"
echo "+----------------------------------------------------+"
echo "Timestamp = ${run_id}"
setup || fail "Test setup failed."
build || fail "Could not build Tinode binaries"
init-db || fail "Could not initialize Tinode database"
run-server || fail "Could not start tinode"
build || fail "Could not build MeetingIM binaries"
init-db || fail "Could not initialize MeetingIM database"
run-server || fail "Could not start meetingim"
# Test requests.
send-requests 16060 $run_id
+1 -1
View File
@@ -60,7 +60,7 @@ func openAdapter(workerId int, jsonconf json.RawMessage) error {
adp = v
}
} else {
return errors.New("store: db adapter is not specified. Please set `store_config.use_adapter` in `tinode.conf`")
return errors.New("store: db adapter is not specified. Please set `store_config.use_adapter` in `meetingim.conf`")
}
}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Reset Tinode password
Reset MeetingIM password
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@ Reset Tinode password
<p>Hello.</p>
<p>You recently requested to reset the password for your <a href="{{.HostUrl}}">Tinode</a> account.
<p>You recently requested to reset the password for your <a href="{{.HostUrl}}">MeetingIM</a> account.
Use the link or code below to reset it. The link and code are valid for the next 24 hours only.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}">Click</a> to reset your password.</blockquote>
@@ -36,7 +36,7 @@ Use the link or code below to reset it. The link and code are valid for the next
<p>If you did not request a password reset, please ignore this message.</p>
<p><a href="https://tinode.co/">Tinode Team</a></p>
<p><a href="https://118im.com/">MeetingIM Team</a></p>
</body>
</html>
@@ -46,7 +46,7 @@ Use the link or code below to reset it. The link and code are valid for the next
Hello.
You recently requested to reset the password for your Tinode account ({{.HostUrl}}).
You recently requested to reset the password for your MeetingIM account ({{.HostUrl}}).
Use the link or code below to reset it. The link and code are valid for the next 24 hours only.
{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}
@@ -62,7 +62,7 @@ In case you have forgotten, here is your login: {{.}}.
If you did not request a password reset, please ignore this message.
Tinode Team
https://tinode.co/
MeetingIM Team
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Reestablecer contraseña de Tinode
Reestablecer contraseña de MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@ Reestablecer contraseña de Tinode
<p>Hola.</p>
<p>Recientemente solicitaste reestablecer la contraseña para tu cuenta <a href="{{.HostUrl}}">Tinode</a>.
<p>Recientemente solicitaste reestablecer la contraseña para tu cuenta <a href="{{.HostUrl}}">MeetingIM</a>.
Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por las siguientes 24 horas.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Clic</a> para reestablecer contraseña.</blockquote>
@@ -36,7 +36,7 @@ Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por l
<p>Si no solicitaste el reestablecimiento de tu contrseña, por favor ignora este mensaje.</p>
<p><a href="https://tinode.co/">Equipo de Tinode</a></p>
<p><a href="https://118im.com/">Equipo de MeetingIM</a></p>
</body>
</html>
@@ -46,7 +46,7 @@ Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por l
Hola.
Recientemente solicitaste reestablecer la contraseña para tu cuenta Tinode ({{.HostUrl}}).
Recientemente solicitaste reestablecer la contraseña para tu cuenta MeetingIM ({{.HostUrl}}).
Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por las siguientes 24 horas.
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
@@ -62,7 +62,7 @@ En caso de que lo hayas olvidado, tu usuario es: {{.}}.
Si no solicitaste el reestablecimiento de tu contrseña, por favor ignora este mensaje.
Equipo de Tinode
https://tinode.co/
Equipo de MeetingIM
https://118im.com/
{{- end}}
+5 -5
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Réinitialiser votre mot de passe Tinode
Réinitialiser votre mot de passe MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@ Réinitialiser votre mot de passe Tinode
<p>Bonjour.</p>
<p>Vous avez récemment demandé à réinitialiser le mot de passe de votre compte <a href="{{.HostUrl}}">Tinode</a>.
<p>Vous avez récemment demandé à réinitialiser le mot de passe de votre compte <a href="{{.HostUrl}}">MeetingIM</a>.
Utilisez le lien ou le code ci-dessous pour le réinitialiser. Le lien et le code sont valides pour les prochaines 24 heures seulement.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Cliquer ici</a> pour réinitialiser votre mot de passe.</blockquote>
@@ -36,7 +36,7 @@ Utilisez le lien ou le code ci-dessous pour le réinitialiser. Le lien et le cod
<p>Si vous n'avez pas demandé la réinitialisation de votre mot de passe, veuillez ignorer ce message.</p>
<p><a href="https://tinode.co/">L'équipe Tinode</a></p>
<p><a href="https://118im.com/">L'équipe MeetingIM</a></p>
</body>
</html>
@@ -62,7 +62,7 @@ Au cas où vous l'auriez oublié, voici votre login : {{.}}.
Si vous n'avez pas demandé la réinitialisation de votre mot de passe, veuillez ignorer ce message.
L'équipe Tinode
https://tinode.co/
L'équipe MeetingIM
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Redefinir senha Tinode
Redefinir senha MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@ Redefinir senha Tinode
<p>Olá.</p>
<p>Você solicitou recentemente a redefinição da sua senha <a href="{{.HostUrl}}">Tinode</a>.
<p>Você solicitou recentemente a redefinição da sua senha <a href="{{.HostUrl}}">MeetingIM</a>.
Use o link ou código abaixo para redefini-lo. O link e o código são válidos apenas pelas próximas 24 horas.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Clic</a> para redefinir sua senha.</blockquote>
@@ -36,7 +36,7 @@ Use o link ou código abaixo para redefini-lo. O link e o código são válidos
<p>Se não solicitaste a redefinição da senha, por favor ignorar essa mensagem.</p>
<p><a href="https://tinode.co/">Equipe Tinode</a></p>
<p><a href="https://118im.com/">Equipe MeetingIM</a></p>
</body>
</html>
@@ -46,7 +46,7 @@ Use o link ou código abaixo para redefini-lo. O link e o código são válidos
Olá.
Você solicitou recentemente a redefinição da sua senha Tinode ({{.HostUrl}}).
Você solicitou recentemente a redefinição da sua senha MeetingIM ({{.HostUrl}}).
Use o link ou código abaixo para redefini-lo. O link e o código são válidos apenas pelas próximas 24 horas.
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
@@ -62,7 +62,7 @@ Em caso de esquecimento, aqui seu login: {{.}}.
Se não solicitaste a redefinição da senha, por favor ignorar essa mensagem..
Equipe Tinode
https://tinode.co/
Equipe MeetingIM
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Изменить пароль Tinode
Изменить пароль MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@
<p>Здравствуйте.</p>
<p>Вы прислали запрос на изменение пароля для вашего аккаунта <a href="{{.HostUrl}}">Tinode</a>.
<p>Вы прислали запрос на изменение пароля для вашего аккаунта <a href="{{.HostUrl}}">MeetingIM</a>.
Используйте ссылку или код ниже, чтобы сбросить его. Ссылка и код действительны только в течение следующих 24 часов.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=RU">Кликните</a> для изменения пароля.</blockquote>
@@ -36,7 +36,7 @@
<p>Если вы не отправляли запрос на изменение пароля, просто игнорируйте это сообщение.</p>
<p><a href="https://tinode.co/">Команда Tinode</a></p>
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
</body>
</html>
@@ -46,7 +46,7 @@
Здравствуйте.
Вы прислали запрос на изменение пароля для вашего аккаунта Tinode ({{.HostUrl}}).
Вы прислали запрос на изменение пароля для вашего аккаунта MeetingIM ({{.HostUrl}}).
Используйте ссылку или код ниже, чтобы сбросить его. Ссылка и код действительны только в течение следующих 24 часов.
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=RU
@@ -62,7 +62,7 @@
Если вы не отправляли запрос на изменение пароля, просто игнорируйте это сообщение.
Команда Tinode
https://tinode.co/
Команда MeetingIM
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Змінити пароль Tinode
Змінити пароль MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@
<p>Доброго дня.</p>
<p>Ви надіслали запит на зміну пароля для вашого акаунта <a href="{{.HostUrl}}">Tinode</a>.
<p>Ви надіслали запит на зміну пароля для вашого акаунта <a href="{{.HostUrl}}">MeetingIM</a>.
Щоб скинути його, скористайтеся посиланням або кодом нижче. Посилання та код дійсні лише протягом наступних 24 годин.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=UK">Натисніть</a> для зміни пароля.</blockquote>
@@ -36,7 +36,7 @@
<p>Якщо ви не надсилали запит на зміну пароля, просто ігноруйте це повідомлення.</p>
<p><a href="https://tinode.co/">Команда Tinode</a></p>
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
</body>
</html>
@@ -46,7 +46,7 @@
Доброго дня.
Ви надіслали запит на зміну пароля для вашого акаунта Tinode ({{.HostUrl}}).
Ви надіслали запит на зміну пароля для вашого акаунта MeetingIM ({{.HostUrl}}).
Щоб скинути його, скористайтеся посиланням або кодом нижче. Посилання та код дійсні лише протягом наступних 24 годин.
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=UK
@@ -62,7 +62,7 @@
Якщо ви не надсилали запит на зміну пароля, просто ігноруйте це повідомлення.
Команда Tinode
https://tinode.co/
Команда MeetingIM
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
Đặt lại mật khẩu Tinode
Đặt lại mật khẩu MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@
<p>Xin chào.</p>
<p>Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản <a href="{{.HostUrl}}">Tinode</a> của bạn.
<p>Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản <a href="{{.HostUrl}}">MeetingIM</a> của bạn.
Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó. Liên kết và mã chỉ có hiệu lực trong 24 giờ tới.</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Bấm</a> để đặt lại mật khẩu.</blockquote>
@@ -36,7 +36,7 @@ Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó.
<p>Nếu bạn không yêu cầu cấp lại mật khẩu, Vui lòng bỏ qua tin nhắn này.</p>
<p><a href="https://tinode.co/">Tinode Team</a></p>
<p><a href="https://118im.com/">MeetingIM Team</a></p>
</body>
</html>
@@ -46,7 +46,7 @@ Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó.
Xin chào.
Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản Tinode ({{.HostUrl}}).
Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản MeetingIM ({{.HostUrl}}).
Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó. Liên kết và mã chỉ có hiệu lực trong 24 giờ tới.
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
@@ -62,7 +62,7 @@ Trong trường hợp bạn quên, thì đây là tên đăng nhập của bạn
Nếu bạn không yêu cầu cấp lại mật khẩu, Vui lòng bỏ qua tin nhắn này.
Tinode Team
https://tinode.co/
MeetingIM Team
https://118im.com/
{{- end}}
@@ -8,7 +8,7 @@
{{define "subject" -}}
重設 Tinode 密碼
重設 MeetingIM 密碼
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@
<p>您好。</p>
<p>您最近請求重設您的 <a href="{{.HostUrl}}">Tinode</a> 帳戶密碼。
<p>您最近請求重設您的 <a href="{{.HostUrl}}">MeetingIM</a> 帳戶密碼。
請使用以下連結或驗證碼進行重設。此連結和驗證碼僅在接下來的 24 小時內有效。</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}">點擊</a>重設您的密碼。</blockquote>
@@ -36,7 +36,7 @@
<p>如果您沒有請求密碼重設,請忽略此訊息。</p>
<p><a href="https://tinode.co/">Tinode 團隊</a></p>
<p><a href="https://118im.com/">MeetingIM 團隊</a></p>
</body>
</html>
@@ -46,7 +46,7 @@
您好。
您最近請求重設您的 Tinode 帳戶密碼 ({{.HostUrl}})。
您最近請求重設您的 MeetingIM 帳戶密碼 ({{.HostUrl}})。
請使用以下連結或驗證碼進行重設。此連結和驗證碼僅在接下來的 24 小時內有效。
{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}
@@ -62,7 +62,7 @@
如果您沒有請求密碼重設,請忽略此訊息。
Tinode 團隊
https://tinode.co/
MeetingIM 團隊
https://118im.com/
{{- end}}
+6 -6
View File
@@ -8,7 +8,7 @@
{{define "subject" -}}
重置 Tinode 密码
重置 MeetingIM 密码
{{- end}}
{{define "body_html" -}}
@@ -17,7 +17,7 @@
<p>您好!</p>
<p>您正在申请重置 <a href="{{.HostUrl}}">Tinode</a> 使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。</p>
<p>您正在申请重置 <a href="{{.HostUrl}}">MeetingIM</a> 使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。</p>
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">重置密码</a></blockquote>
@@ -35,7 +35,7 @@
<p>如非您没有申请重置密码,请忽略这条消息。</p>
<p><a href="https://tinode.co/">Tinode 团队</a></p>
<p><a href="https://118im.com/">MeetingIM 团队</a></p>
</body>
</html>
@@ -45,7 +45,7 @@
您好!
您正在申请重置 Tinode ({{.HostUrl}}) 账号密码。
您正在申请重置 MeetingIM ({{.HostUrl}}) 账号密码。
使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
@@ -61,7 +61,7 @@
如非您没有申请重置密码,请忽略这条消息。
Tinode 团队
https://tinode.co/
MeetingIM 团队
https://118im.com/
{{- end}}
+8 -8
View File
@@ -14,7 +14,7 @@
*/}}
{{define "subject" -}}
Tinode registration: confirm email
MeetingIM registration: confirm email
{{- end}}
{{define "body_html" -}}
@@ -24,7 +24,7 @@ Tinode registration: confirm email
<p>Hello.</p>
<p>You're receiving this message because someone used your email to register at
<a href="{{.HostUrl}}">Tinode</a>.</p>
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Click to confirm</a>
or go to
@@ -33,9 +33,9 @@ and enter the following code:</p>
<blockquote><big>{{.Code}}</big></blockquote>
<p>You may need to enter login and password.</p>
<p>If you did not register at Tinode just ignore this message.</p>
<p>If you did not register at MeetingIM just ignore this message.</p>
<p><a href="https://tinode.co/">Tinode Team</a></p>
<p><a href="https://118im.com/">MeetingIM Team</a></p>
</body>
</html>
@@ -45,7 +45,7 @@ and enter the following code:</p>
Hello.
You're receiving this message because someone used your email to register at Tinode ({{.HostUrl}}).
You're receiving this message because someone used your email to register at MeetingIM ({{.HostUrl}}).
Click the link {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} to confirm or go to {{.HostUrl}}#cred?what=email
and enter the following code:
@@ -54,9 +54,9 @@ and enter the following code:
You may need to enter login and password.
If you did not register at Tinode just ignore this message.
If you did not register at MeetingIM just ignore this message.
Tinode Team
https://tinode.co/
MeetingIM Team
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Registro Tinode: Correo de confirmación
Registro MeetingIM: Correo de confirmación
{{- end}}
{{define "body_html" -}}
@@ -15,7 +15,7 @@ Registro Tinode: Correo de confirmación
<p>Hola.</p>
<p>Estás recibiendo este correo porque alguien uso tu correo para registrarse en
<a href="{{.HostUrl}}">Tinode</a>.</p>
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Clic para confirmar</a>
o ve a
@@ -24,9 +24,9 @@ e ingresa el siguiente código:</p>
<blockquote><big>{{.Code}}</big></blockquote>
<p>Necesitas ingresar tu usuario y contraseña.</p>
<p>Si tú no te registraste en Tinode solo ignora este mensaje.</p>
<p>Si tú no te registraste en MeetingIM solo ignora este mensaje.</p>
<p><a href="https://tinode.co/">Equipo de Tinode</a></p>
<p><a href="https://118im.com/">Equipo de MeetingIM</a></p>
</body>
</html>
@@ -36,7 +36,7 @@ e ingresa el siguiente código:</p>
Hola.
Estás recibiendo este correo porque alguien uso tu correo para registrarse en Tinode ({{.HostUrl}}).
Estás recibiendo este correo porque alguien uso tu correo para registrarse en MeetingIM ({{.HostUrl}}).
Da clic en el enlace {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} para confirmar o ve a {{.HostUrl}}#cred?what=email
e ingresa el siguiente código:
@@ -45,9 +45,9 @@ e ingresa el siguiente código:
Necesitas ingresar tu usuario y contraseña.
Si tú no te registraste en Tinode solo ignora este mensaje.
Si tú no te registraste en MeetingIM solo ignora este mensaje.
Equipo de Tinode
https://tinode.co/
Equipo de MeetingIM
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Tinode enregistrement : confirmer l'adresse e-mail
MeetingIM enregistrement : confirmer l'adresse e-mail
{{- end}}
{{define "body_html" -}}
@@ -15,7 +15,7 @@ Tinode enregistrement : confirmer l'adresse e-mail
<p>Bonjour.</p>
<p>Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site
<a href="{{.HostUrl}}">Tinode</a>.</p>
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Cliquer ici pour confimer</a>
ou rendez-vous à l'adresse
@@ -24,9 +24,9 @@ et entrez le code suivant :</p>
<blockquote><big>{{.Code}}</big></blockquote>
<p>Vous devrez peut-être entrer un login et un mot de passe.</p>
<p>Si vous ne vous êtes pas inscrit à Tinode, ignorez ce message.</p>
<p>Si vous ne vous êtes pas inscrit à MeetingIM, ignorez ce message.</p>
<p><a href="https://tinode.co/">L'équipe Tinode</a></p>
<p><a href="https://118im.com/">L'équipe MeetingIM</a></p>
</body>
</html>
@@ -36,7 +36,7 @@ et entrez le code suivant :</p>
Bonjour.
Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site Tinode ({{.HostUrl}}).
Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site MeetingIM ({{.HostUrl}}).
Cliquer sur le lien {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} pour confimer ou rendez-vous à l'adresse {{.HostUrl}}#cred?what=email
et entrez le code suivant :
@@ -45,9 +45,9 @@ et entrez le code suivant :
Vous devrez peut-être entrer un login et un mot de passe.
Si vous ne vous êtes pas inscrit à Tinode, ignorez ce message.
Si vous ne vous êtes pas inscrit à MeetingIM, ignorez ce message.
L'équipe Tinode
https://tinode.co/
L'équipe MeetingIM
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Registro Tinode: E-mail de confirmação
Registro MeetingIM: E-mail de confirmação
{{- end}}
{{define "body_html" -}}
@@ -15,7 +15,7 @@ Registro Tinode: E-mail de confirmação
<p>Olá.</p>
<p> Você está recebendo esse e-mail porque alguém usou seu e-mail para registrar-se em
<a href="{{.HostUrl}}">Tinode</a>.</p>
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Clique para confirmar</a>
ou acesse
@@ -24,9 +24,9 @@ e entre com o seguinte código:</p>
<blockquote><big>{{.Code}}</big></blockquote>
<p>Necessário entrar com login e senha.</p>
<p>Se você não se registrou em Tinode apenas ignore essa mensagem.</p>
<p>Se você não se registrou em MeetingIM apenas ignore essa mensagem.</p>
<p><a href="https://tinode.co/">Equipe Tinode</a></p>
<p><a href="https://118im.com/">Equipe MeetingIM</a></p>
</body>
</html>
@@ -36,7 +36,7 @@ e entre com o seguinte código:</p>
Olá.
Você está recebendo este e-mail porque alguém usou seu e-mail para registrar-se em Tinode ({{.HostUrl}}).
Você está recebendo este e-mail porque alguém usou seu e-mail para registrar-se em MeetingIM ({{.HostUrl}}).
Clique no link {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} para confirmar {{.HostUrl}}#cred?what=email
e entre com o seguinte código:
@@ -45,9 +45,9 @@ e entre com o seguinte código:
Necessário entrar com login e senha.
Se você não se registrou em Tinode apenas ignore essa mensagem..
Se você não se registrou em MeetingIM apenas ignore essa mensagem..
Equipe Tinode
https://tinode.co/
Equipe MeetingIM
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Регистрация Tinode: подтвердите емейл
Регистрация MeetingIM: подтвердите емейл
{{- end}}
{{define "body_html" -}}
@@ -14,7 +14,7 @@
<p>Здравствуйте.</p>
<p>Вы получили это сообщение потому, что зарегистрировались в <a href="{{.HostUrl}}">Tinode</a>.</p>
<p>Вы получили это сообщение потому, что зарегистрировались в <a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=RU">Кликните здесь чтобы подтвердить</a>
регистрацию или перейдите по сслыке
@@ -23,9 +23,9 @@
<blockquote><big>{{.Code}}</big></blockquote>
<p>Возможно, вам потребуется ввести логин и пароль.</p>
<p>Если вы не регистрировались в Tinode, просто игнорируйте это сообщение.</p>
<p>Если вы не регистрировались в MeetingIM, просто игнорируйте это сообщение.</p>
<p><a href="https://tinode.co/">Команда Tinode</a></p>
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
</body>
</html>
@@ -35,7 +35,7 @@
Здравствуйте.
Вы получили это сообщение потому, что зарегистрировались в Tinode ({{.HostUrl}}).
Вы получили это сообщение потому, что зарегистрировались в MeetingIM ({{.HostUrl}}).
Кликните на {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=RU чтобы подтвердить
регистрацию или перейдите по сслыке {{.HostUrl}}#cred?what=email">{{.HostUrl}}#cred?method=email&hl=RU
@@ -45,9 +45,9 @@
Возможно, вам также потребуется ввести логин и пароль.
Если вы не регистрировались в Tinode, просто игнорируйте это сообщение.
Если вы не регистрировались в MeetingIM, просто игнорируйте это сообщение.
Команда Tinode
https://tinode.co/
Команда MeetingIM
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Реєстрація Tinode: підтвердіть емейл
Реєстрація MeetingIM: підтвердіть емейл
{{- end}}
{{define "body_html" -}}
@@ -14,7 +14,7 @@
<p>Доброго дня.</p>
<p>Ви отримали це повідомлення тому, що зареєструвалися в <a href="{{.HostUrl}}">Tinode</a>.</p>
<p>Ви отримали це повідомлення тому, що зареєструвалися в <a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=UK">Натисніть тут, щоб підтвердити</a>
реєстрацію або перейдіть за посиланням
@@ -23,9 +23,9 @@
<blockquote><big>{{.Code}}</big></blockquote>
<p>Можливо, вам потрібно буде ввести логін та пароль.</p>
<p>Якщо ви не реєструвалися в Tinode, просто ігноруйте це повідомлення.</p>
<p>Якщо ви не реєструвалися в MeetingIM, просто ігноруйте це повідомлення.</p>
<p><a href="https://tinode.co/">Команда Tinode</a></p>
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
</body>
</html>
@@ -35,7 +35,7 @@
Доброго дня.
Ви отримали це повідомлення тому, що зареєструвалися в Tinode ({{.HostUrl}}).
Ви отримали це повідомлення тому, що зареєструвалися в MeetingIM ({{.HostUrl}}).
Натисніть на {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=UK щоб підтвердити
реєстрацію або перейдіть за посиланням {{.HostUrl}}#cred?what=email">{{.HostUrl}}#cred?method=email&hl=UK
@@ -45,9 +45,9 @@
Можливо, вам потрібно буде ввести логін та пароль.
Якщо ви не реєструвалися в Tinode, просто ігноруйте це повідомлення.
Якщо ви не реєструвалися в MeetingIM, просто ігноруйте це повідомлення.
Команда Tinode
https://tinode.co/
Команда MeetingIM
https://118im.com/
{{- end}}
+8 -8
View File
@@ -5,7 +5,7 @@
*/}}
{{define "subject" -}}
Xác thực đăng ký tài khoản Tinode
Xác thực đăng ký tài khoản MeetingIM
{{- end}}
{{define "body_html" -}}
@@ -15,7 +15,7 @@ Xác thực đăng ký tài khoản Tinode
<p>Xin chào.</p>
<p>Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại
<a href="{{.HostUrl}}">Tinode</a>.</p>
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Bấm để xác nhận</a>
hoặc đi tới liên kết
@@ -24,9 +24,9 @@ và nhập mã xác thực:</p>
<blockquote><big>{{.Code}}</big></blockquote>
<p>Có thể bạn cần nhập tên đăng nhập và mật khẩu.</p>
<p>Nếu bạn không đăng ký tài khoản tại Tinode vui lòng bỏ qua tin nhắn này.</p>
<p>Nếu bạn không đăng ký tài khoản tại MeetingIM vui lòng bỏ qua tin nhắn này.</p>
<p><a href="https://tinode.co/">Tinode Team</a></p>
<p><a href="https://118im.com/">MeetingIM Team</a></p>
</body>
</html>
@@ -36,7 +36,7 @@ và nhập mã xác thực:</p>
Xin chào.
Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại Tinode ({{.HostUrl}}).
Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại MeetingIM ({{.HostUrl}}).
Bấm vào liên kết {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} để xác nhận hoặc đi tới {{.HostUrl}}#cred?what=email
và nhập mã xác thực
@@ -45,9 +45,9 @@ và nhập mã xác thực
Có thể bạn sẽ cần nhập tên đăng nhập và mật khẩu.
Nếu bạn không đăng ký tài khoản tại Tinode vui lòng bỏ qua tin nhắn này.
Nếu bạn không đăng ký tài khoản tại MeetingIM vui lòng bỏ qua tin nhắn này.
Tinode Team
https://tinode.co/
MeetingIM Team
https://118im.com/
{{- end}}
+8 -8
View File
@@ -15,7 +15,7 @@
*/}}
{{define "subject" -}}
Tinode 註冊:確認電子郵件
MeetingIM 註冊:確認電子郵件
{{- end}}
{{define "body_html" -}}
@@ -25,7 +25,7 @@ Tinode 註冊:確認電子郵件
<p>您好。</p>
<p>您收到此訊息是因為有人使用您的電子郵件在
<a href="{{.HostUrl}}">Tinode</a> 註冊。</p>
<a href="{{.HostUrl}}">MeetingIM</a> 註冊。</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">點擊確認</a>
或前往
@@ -34,9 +34,9 @@ Tinode 註冊:確認電子郵件
<blockquote><big>{{.Code}}</big></blockquote>
<p>您可能需要輸入登入名稱和密碼。</p>
<p>如果您沒有在 Tinode 註冊,請忽略此訊息。</p>
<p>如果您沒有在 MeetingIM 註冊,請忽略此訊息。</p>
<p><a href="https://tinode.co/">Tinode 團隊</a></p>
<p><a href="https://118im.com/">MeetingIM 團隊</a></p>
</body>
</html>
@@ -46,7 +46,7 @@ Tinode 註冊:確認電子郵件
您好。
您收到此訊息是因為有人使用您的電子郵件在 Tinode ({{.HostUrl}}) 註冊。
您收到此訊息是因為有人使用您的電子郵件在 MeetingIM ({{.HostUrl}}) 註冊。
點擊連結 {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} 進行確認,或前往 {{.HostUrl}}#cred?what=email
並輸入以下驗證碼:
@@ -55,9 +55,9 @@ Tinode 註冊:確認電子郵件
您可能需要輸入登入名稱和密碼。
如果您沒有在 Tinode 註冊,請忽略此訊息。
如果您沒有在 MeetingIM 註冊,請忽略此訊息。
Tinode 團隊
https://tinode.co/
MeetingIM 團隊
https://118im.com/
{{- end}}
+8 -8
View File
@@ -14,7 +14,7 @@
*/}}
{{define "subject" -}}
Tinode 注册: 确认邮件
MeetingIM 注册: 确认邮件
{{- end}}
{{define "body_html" -}}
@@ -23,7 +23,7 @@ Tinode 注册: 确认邮件
<p>您好!</p>
<p>您收到此消息是因为您注册了<a href="{{.HostUrl}}">Tinode</a>。</p>
<p>您收到此消息是因为您注册了<a href="{{.HostUrl}}">MeetingIM</a>。</p>
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">确认注册</a>
或者跳转至链接
@@ -32,9 +32,9 @@ Tinode 注册: 确认邮件
<blockquote><big>{{.Code}}</big></blockquote>
<p>您可能需要输入登录名和密码。</p>
<p>如果您没有注册Tinode,请忽略这条消息。</p>
<p>如果您没有注册MeetingIM,请忽略这条消息。</p>
<p><a href="https://tinode.co/">Tinode 团队</a></p>
<p><a href="https://118im.com/">MeetingIM 团队</a></p>
</body>
</html>
@@ -44,7 +44,7 @@ Tinode 注册: 确认邮件
您好!
您收到此消息是因为您注册了 Tinode ({{.HostUrl}})。
您收到此消息是因为您注册了 MeetingIM ({{.HostUrl}})。
点击链接 {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} 确认注册或者跳转至链接 {{.HostUrl}}#cred?what=email
并输入验证码:
@@ -53,9 +53,9 @@ Tinode 注册: 确认邮件
您可能需要输入登录名和密码。
如果您没有注册Tinode,请忽略这条消息。
如果您没有注册MeetingIM,请忽略这条消息。
Tinode 团队
https://tinode.co/
MeetingIM 团队
https://118im.com/
{{- end}}
+1 -1
View File
@@ -4,5 +4,5 @@
Universal confirmation and password reset template for SMS.
*/}}
Tinode confirmation code: {{.Code}}
MeetingIM confirmation code: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Universal confirmation and password reset template for SMS.
*/}}
Código de confirmación de Tinode: {{.Code}}
Código de confirmación de MeetingIM: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Modèle universel de confirmation et de réinitialisation du mot de passe pour SMS.
*/}}
Code de confirmation Tinode : {{.Code}}
Code de confirmation MeetingIM : {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Modelo universal de confirmação e redefinição de senha para SMS.
*/}}
Código de confirmação Tinode: {{.Code}}
Código de confirmação MeetingIM: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Универсальный шаблон подтверждения и сброса пароля для СМС.
*/}}
Код подтверждения Tinode: {{.Code}}
Код подтверждения MeetingIM: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Універсальний шаблон підтвердження та скидання пароля для СМС.
*/}}
Код підтвердження Tinode: {{.Code}}
Код підтвердження MeetingIM: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Universal confirmation and password reset template for SMS.
*/}}
Mã xác thực từ Tinode: {{.Code}}
Mã xác thực từ MeetingIM: {{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
通用確認和密碼重設簡訊模板。
*/}}
Tinode 確認驗證碼:{{.Code}}
MeetingIM 確認驗證碼:{{.Code}}
{{.HostUrl}}
+1 -1
View File
@@ -4,5 +4,5 @@
Universal confirmation and password reset template for SMS.
*/}}
【Tinode】验证码: {{.Code}}
【MeetingIM】验证码: {{.Code}}
{{.HostUrl}}
+2 -2
View File
@@ -163,7 +163,7 @@
"required": [],
"config": {
"host_url": "http://64.118.137.182:6060/",
"sender": "Tinode <noreply@118im.com>",
"sender": "MeetingIM <noreply@118im.com>",
"validation_templ": "server/templ/email-validation-en.templ",
"reset_secret_templ": "server/templ/email-password-reset-en.templ",
"debug_response": "123456",
@@ -175,7 +175,7 @@
"required": [],
"config": {
"host_url": "http://64.118.137.182:6060/",
"sender": "Tinode",
"sender": "MeetingIM",
"universal_templ": "server/templ/sms-universal-en.templ",
"debug_response": "123456",
"max_retries": 3
+2 -2
View File
@@ -124,7 +124,7 @@ func TestServeIMPasswordResetVerify(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/challenges/challenge-1/verify",
bytes.NewBufferString(`{"code":"123456"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
@@ -197,7 +197,7 @@ func TestServeIMPasswordResetConfirm(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/confirm",
bytes.NewBufferString(`{"reset_token":"challenge-1.`+token+`","new_password":"new-password"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
+1 -1
View File
@@ -51,7 +51,7 @@ const (
// Default code length when one is not provided in the config
defaultCodeLength = 6
defaultSender = "Tinode"
defaultSender = "MeetingIM"
)
func (v *validator) Init(jsonconf string) error {