Support meeting note upload registration
This commit is contained in:
@@ -2266,6 +2266,16 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
- 返回我的最终响应和会议版本,并通过 `meeting.response.updated` 实时通知参会人;重复提交同状态幂等。
|
- 返回我的最终响应和会议版本,并通过 `meeting.response.updated` 实时通知参会人;重复提交同状态幂等。
|
||||||
- 会议过期返回 `MEETING_CLOSED` 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
|
- 会议过期返回 `MEETING_CLOSED` 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
|
||||||
|
|
||||||
|
### 9.4.1 会议笔记
|
||||||
|
|
||||||
|
**状态:新增。**
|
||||||
|
|
||||||
|
- 列表:`GET /v1/im/meeting-notes?query=&cursor=&limit=20` 返回可查看会议笔记的会议列表、`note_status/file_count/permissions`。
|
||||||
|
- 详情:`GET /v1/im/meetings/{meeting_id}/notes` 返回会议信息和 `files[]`。
|
||||||
|
- 上传并登记:调用通用文件上传 `POST /v0/file/u/`,multipart 包含 `file=@...`、`purpose=meeting_note`、`meeting_id=<meeting_id>`、`id=<本次上传幂等ID>`,请求头按文件上传鉴权携带 `X-MeetingIM-APIKey`、`X-MeetingIM-Auth`。服务端上传成功后会立即登记会议笔记文件,响应 `ctrl.params` 包含 `url,file_id,purpose,meeting_id,note_file_id,note_file_status,version`。
|
||||||
|
- 兼容绑定:若客户端先完成普通文件上传,也可以调用 `POST /v1/im/meetings/{meeting_id}/notes/files`,Body `{"file_id":"..."}`,并携带 `Idempotency-Key` 完成登记。
|
||||||
|
- 替换/删除:`PUT /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}`、`DELETE /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}?version=n`。只有记录人/发起人按权限可上传、替换和删除。
|
||||||
|
|
||||||
### 9.5 日报.png
|
### 9.5 日报.png
|
||||||
|
|
||||||
<img src="images/日报.png" alt="日报" width="360">
|
<img src="images/日报.png" alt="日报" width="360">
|
||||||
@@ -2386,6 +2396,10 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
| `POST /v1/im/meetings/{id}/cancel` | `{"version":2,"reason":"时间冲突"}` | `{"meeting_id":"mtg_1","state":"cancelled","version":3}` |
|
| `POST /v1/im/meetings/{id}/cancel` | `{"version":2,"reason":"时间冲突"}` | `{"meeting_id":"mtg_1","state":"cancelled","version":3}` |
|
||||||
| `POST /v1/im/meetings/{id}/reminders` | `{}` | `{"meeting_id":"mtg_1","items":[{"user_id":"usr...","status":"sent"}]}` |
|
| `POST /v1/im/meetings/{id}/reminders` | `{}` | `{"meeting_id":"mtg_1","items":[{"user_id":"usr...","status":"sent"}]}` |
|
||||||
| `PUT /v1/im/meetings/{meeting_id}/my-response` | `{"response":"accepted","version":1}` | `{"meeting_id":"mtg_1","my_response":"accepted","version":2,"response_at":"2026-08-20T09:00:00Z"}` |
|
| `PUT /v1/im/meetings/{meeting_id}/my-response` | `{"response":"accepted","version":1}` | `{"meeting_id":"mtg_1","my_response":"accepted","version":2,"response_at":"2026-08-20T09:00:00Z"}` |
|
||||||
|
| `GET /v1/im/meeting-notes?query=&cursor=` | Query only | `{"items":[{"meeting_id":"mtg_1","title":"周会","note_status":"available","file_count":1,"permissions":{"can_upload":true,"can_download":true}}],"next_cursor":"","has_more":false}` |
|
||||||
|
| `GET /v1/im/meetings/{meeting_id}/notes` | Path meeting ID | `{"meeting_id":"mtg_1","note_status":"available","files":[{"note_file_id":"mnote_1","file_id":"file_1","file_name":"会议纪要.pdf","mime_type":"application/pdf","permissions":{"can_download":true}}]}` |
|
||||||
|
| `POST /v0/file/u/` 会议笔记上传并登记 | Multipart `purpose=meeting_note&meeting_id=mtg_1&id=upload_1&file=@note.pdf` | `{"ctrl":{"code":200,"params":{"url":"/v0/file/s/file_1.pdf","file_id":"file_1","purpose":"meeting_note","meeting_id":"mtg_1","note_file_id":"mnote_1","note_file_status":"active","version":1}}}` |
|
||||||
|
| `POST /v1/im/meetings/{meeting_id}/notes/files` | Header `Idempotency-Key`;Body `{"file_id":"file_1"}` | `{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_1","status":"active","version":1}` |
|
||||||
| `GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor=` | Query only | `{"items":[{"report_id":"dr_1","report_date":"2026-08-20","content_short":"今日完成联调和问题回归...","status":"draft","draft_updated_at":"2026-08-20T09:00:00Z","submitted_at":null,"read_state":"unread"}],"next_cursor":"","permissions":{"can_create":true,"makeup_allowed_days":7}}` |
|
| `GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor=` | Query only | `{"items":[{"report_id":"dr_1","report_date":"2026-08-20","content_short":"今日完成联调和问题回归...","status":"draft","draft_updated_at":"2026-08-20T09:00:00Z","submitted_at":null,"read_state":"unread"}],"next_cursor":"","permissions":{"can_create":true,"makeup_allowed_days":7}}` |
|
||||||
| `GET /v1/im/reports/daily?scope=managed&date=&status=submitted\|missing&department_id=&cursor=` | Query only | `{"items":[{"report_id":"dr_1","author":{"user_id":"usr...","display_name":"张三"},"report_date":"2026-08-20","content_short":"完成日报提交和项目同步...","status":"submitted","submitted_at":"2026-08-20T09:00:00Z","read_state":"unread"}],"next_cursor":"","permissions":{"can_remind_unsubmitted":true,"can_view_managed":true}}` |
|
| `GET /v1/im/reports/daily?scope=managed&date=&status=submitted\|missing&department_id=&cursor=` | Query only | `{"items":[{"report_id":"dr_1","author":{"user_id":"usr...","display_name":"张三"},"report_date":"2026-08-20","content_short":"完成日报提交和项目同步...","status":"submitted","submitted_at":"2026-08-20T09:00:00Z","read_state":"unread"}],"next_cursor":"","permissions":{"can_remind_unsubmitted":true,"can_view_managed":true}}` |
|
||||||
| `POST /v1/im/reports/daily` | `{"report_date":"2026-08-20"}` | `{"report_id":"dr_1","report_date":"2026-08-20","content":"","attachments":[],"status":"draft","version":1,"permissions":{"can_edit":true,"can_submit":true,"can_view":true}}` |
|
| `POST /v1/im/reports/daily` | `{"report_date":"2026-08-20"}` | `{"report_id":"dr_1","report_date":"2026-08-20","content":"","attachments":[],"status":"draft","version":1,"permissions":{"can_edit":true,"can_submit":true,"can_view":true}}` |
|
||||||
|
|||||||
+52
-1
@@ -37,6 +37,8 @@ import (
|
|||||||
// See https://www.iana.org/assignments/media-types/media-types.xhtml
|
// See https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||||
var allowedMimeTypes = []string{"application/", "audio/", "font/", "image/", "text/", "video/"}
|
var allowedMimeTypes = []string{"application/", "audio/", "font/", "image/", "text/", "video/"}
|
||||||
|
|
||||||
|
const fileUploadPurposeMeetingNote = "meeting_note"
|
||||||
|
|
||||||
// RequestPrincipal is a tenant-scoped identity authenticated for HTTP/gRPC file access.
|
// RequestPrincipal is a tenant-scoped identity authenticated for HTTP/gRPC file access.
|
||||||
type RequestPrincipal struct {
|
type RequestPrincipal struct {
|
||||||
TenantID types.TenantID
|
TenantID types.TenantID
|
||||||
@@ -249,6 +251,11 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeHttpResponse(ErrAuthRequired(msgID, "", now, now), nil)
|
writeHttpResponse(ErrAuthRequired(msgID, "", now, now), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
meetingID, meetingNoteIdempotencyKey, err := meetingNoteUploadParams(req, principal)
|
||||||
|
if err != nil {
|
||||||
|
writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if uploads are handled elsewhere.
|
// Check if uploads are handled elsewhere.
|
||||||
headers, statusCode, err := mh.Headers(principal.TenantID, req.Method, req.URL, req.Header, true)
|
headers, statusCode, err := mh.Headers(principal.TenantID, req.Method, req.URL, req.Header, true)
|
||||||
@@ -354,16 +361,60 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
logs.Warn.Printf("media upload: failed to enqueue file lifecycle event file=%s: %v", fdef.Id, err)
|
logs.Warn.Printf("media upload: failed to enqueue file lifecycle event file=%s: %v", fdef.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]string{"url": url}
|
params := map[string]any{"url": url, "file_id": fdef.Uid().String()}
|
||||||
if globals.mediaGcPeriod > 0 {
|
if globals.mediaGcPeriod > 0 {
|
||||||
// How long this file is guaranteed to exist without being attached to a message or a topic.
|
// How long this file is guaranteed to exist without being attached to a message or a topic.
|
||||||
params["expires"] = now.Add(globals.mediaGcPeriod).Format(types.TimeFormatRFC3339)
|
params["expires"] = now.Add(globals.mediaGcPeriod).Format(types.TimeFormatRFC3339)
|
||||||
}
|
}
|
||||||
|
if meetingID != "" {
|
||||||
|
noteFile, err := createMeetingNoteFromUpload(principal, meetingID, meetingNoteIdempotencyKey, fdef)
|
||||||
|
if err != nil {
|
||||||
|
writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params["purpose"] = fileUploadPurposeMeetingNote
|
||||||
|
params["meeting_id"] = noteFile.MeetingID
|
||||||
|
params["note_file_id"] = noteFile.NoteFileID
|
||||||
|
params["note_file_status"] = noteFile.Status
|
||||||
|
params["version"] = noteFile.Version
|
||||||
|
}
|
||||||
|
|
||||||
writeHttpResponse(NoErrParams(msgID, "", now, params), nil)
|
writeHttpResponse(NoErrParams(msgID, "", now, params), nil)
|
||||||
logs.Info.Println("media upload: ok", fdef.Id, fdef.Location)
|
logs.Info.Println("media upload: ok", fdef.Id, fdef.Location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func meetingNoteUploadParams(req *http.Request, principal RequestPrincipal) (string, string, error) {
|
||||||
|
if strings.TrimSpace(req.FormValue("purpose")) != fileUploadPurposeMeetingNote {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
if principal.UID.IsZero() {
|
||||||
|
return "", "", types.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
if store.MeetingNotes == nil {
|
||||||
|
return "", "", types.ErrUnsupported
|
||||||
|
}
|
||||||
|
meetingID := strings.TrimSpace(req.FormValue("meeting_id"))
|
||||||
|
if meetingID == "" {
|
||||||
|
return "", "", types.ErrMalformed
|
||||||
|
}
|
||||||
|
idempotencyKey := strings.TrimSpace(req.Header.Get("Idempotency-Key"))
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
idempotencyKey = strings.TrimSpace(req.FormValue("id"))
|
||||||
|
}
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
return "", "", types.ErrMalformed
|
||||||
|
}
|
||||||
|
return meetingID, idempotencyKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMeetingNoteFromUpload(principal RequestPrincipal, meetingID, idempotencyKey string, fdef *types.FileDef) (*types.MeetingNoteFile, error) {
|
||||||
|
if fdef == nil || fdef.Uid().IsZero() {
|
||||||
|
return nil, types.ErrMalformed
|
||||||
|
}
|
||||||
|
noteFileID := "mnote_" + store.Store.GetUidString()
|
||||||
|
return store.MeetingNotes.CreateFile(principal.TenantID, principal.UID, meetingID, noteFileID, fdef.Uid().String(), idempotencyKey)
|
||||||
|
}
|
||||||
|
|
||||||
// LargeFileServe is the gRPC equivalent of largeFileServeHTTP.
|
// LargeFileServe is the gRPC equivalent of largeFileServeHTTP.
|
||||||
func (*grpcNodeServer) LargeFileServe(req *pbx.FileDownReq, stream pbx.Node_LargeFileServeServer) error {
|
func (*grpcNodeServer) LargeFileServe(req *pbx.FileDownReq, stream pbx.Node_LargeFileServeServer) error {
|
||||||
now := types.TimeNow()
|
now := types.TimeNow()
|
||||||
|
|||||||
@@ -1,19 +1,51 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/tinode/chat/server/auth"
|
"github.com/tinode/chat/server/auth"
|
||||||
"github.com/tinode/chat/server/auth/mock_auth"
|
"github.com/tinode/chat/server/auth/mock_auth"
|
||||||
|
"github.com/tinode/chat/server/media"
|
||||||
"github.com/tinode/chat/server/store"
|
"github.com/tinode/chat/server/store"
|
||||||
"github.com/tinode/chat/server/store/mock_store"
|
"github.com/tinode/chat/server/store/mock_store"
|
||||||
"github.com/tinode/chat/server/store/types"
|
"github.com/tinode/chat/server/store/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type testLargeFileMediaHandler struct {
|
||||||
|
def *types.FileDef
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testLargeFileMediaHandler) Init(string) error { return nil }
|
||||||
|
func (h *testLargeFileMediaHandler) Headers(types.TenantID, string, *url.URL, http.Header, bool) (http.Header, int, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
func (h *testLargeFileMediaHandler) Upload(def *types.FileDef, file io.Reader) (string, int64, error) {
|
||||||
|
data, _ := io.ReadAll(file)
|
||||||
|
h.def = def
|
||||||
|
h.data = string(data)
|
||||||
|
def.Location = "/v0/file/s/" + def.Uid().String() + ".txt"
|
||||||
|
def.ETag = "etag"
|
||||||
|
return def.Location, int64(len(data)), nil
|
||||||
|
}
|
||||||
|
func (h *testLargeFileMediaHandler) Download(types.TenantID, string) (*types.FileDef, media.ReadSeekCloser, error) {
|
||||||
|
return nil, nil, types.ErrUnsupported
|
||||||
|
}
|
||||||
|
func (h *testLargeFileMediaHandler) Delete([]string) error { return nil }
|
||||||
|
func (h *testLargeFileMediaHandler) GetIdFromUrl(string) types.Uid {
|
||||||
|
return types.ZeroUid
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthFileRequestFromSessionRequiresTenant(t *testing.T) {
|
func TestAuthFileRequestFromSessionRequiresTenant(t *testing.T) {
|
||||||
oldSessionStore := globals.sessionStore
|
oldSessionStore := globals.sessionStore
|
||||||
globals.sessionStore = NewSessionStore(time.Minute)
|
globals.sessionStore = NewSessionStore(time.Minute)
|
||||||
@@ -137,3 +169,90 @@ func TestLargeFileGarbageCollectTenantsScopesDeletes(t *testing.T) {
|
|||||||
|
|
||||||
largeFileGarbageCollectTenants([]types.TenantID{7, 8}, olderThan, 25)
|
largeFileGarbageCollectTenants([]types.TenantID{7, 8}, olderThan, 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLargeFileReceiveMeetingNoteUploadCreatesNoteFile(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
oldStore := store.Store
|
||||||
|
oldFiles := store.Files
|
||||||
|
oldMeetingNotes := store.MeetingNotes
|
||||||
|
oldKafkaBus := kafkaBus
|
||||||
|
defer func() {
|
||||||
|
store.Store = oldStore
|
||||||
|
store.Files = oldFiles
|
||||||
|
store.MeetingNotes = oldMeetingNotes
|
||||||
|
kafkaBus = oldKafkaBus
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler := &testLargeFileMediaHandler{}
|
||||||
|
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||||
|
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
|
||||||
|
files := mock_store.NewMockFilePersistenceInterface(ctrl)
|
||||||
|
notes := &testMeetingNotesStore{}
|
||||||
|
store.Store = mockStore
|
||||||
|
store.Files = files
|
||||||
|
store.MeetingNotes = notes
|
||||||
|
kafkaBus = nil
|
||||||
|
|
||||||
|
fileID := types.Uid(101)
|
||||||
|
mockStore.EXPECT().GetMediaHandler().Return(handler).AnyTimes()
|
||||||
|
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
|
||||||
|
mockStore.EXPECT().GetUidString().Return(fileID.String())
|
||||||
|
mockStore.EXPECT().GetUidString().Return("note-1")
|
||||||
|
tokenAuth.EXPECT().Authenticate(auth.AuthContext{RemoteAddr: "192.0.2.1:12345"}, []byte("token-1")).
|
||||||
|
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil)
|
||||||
|
files.EXPECT().FinishUpload(types.TenantID(7), gomock.Any(), true, int64(11)).DoAndReturn(
|
||||||
|
func(_ types.TenantID, fd *types.FileDef, _ bool, size int64) (*types.FileDef, error) {
|
||||||
|
fd.Status = types.UploadCompleted
|
||||||
|
fd.Size = size
|
||||||
|
return fd, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
if err := writer.WriteField("id", "upload-meeting-note-1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := writer.WriteField("purpose", fileUploadPurposeMeetingNote); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := writer.WriteField("meeting_id", "mtg_test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
part, err := writer.CreateFormFile("file", "notes.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := part.Write([]byte("hello notes")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v0/file/u/", &body)
|
||||||
|
req.RemoteAddr = "192.0.2.1:12345"
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||||
|
req.Header.Set("X-MeetingIM-Auth", "token "+base64.StdEncoding.EncodeToString([]byte("token-1")))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
largeFileReceiveHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var resp ServerComMessage
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("decode response: %v body=%s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
params := resp.Ctrl.Params.(map[string]any)
|
||||||
|
if params["file_id"] != fileID.String() || params["purpose"] != fileUploadPurposeMeetingNote ||
|
||||||
|
params["meeting_id"] != "mtg_test" || params["note_file_id"] != "mnote_note-1" {
|
||||||
|
t.Fatalf("unexpected upload params: %#v", params)
|
||||||
|
}
|
||||||
|
if handler.data != "hello notes" || notes.createdFileID != fileID.String() || notes.createdMeetingID != "mtg_test" || notes.createdIdempotencyKey != "upload-meeting-note-1" {
|
||||||
|
t.Fatalf("unexpected side effects: handler=%#v notes=%#v", handler, notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-5
@@ -270,11 +270,14 @@ type testMeetingsStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testMeetingNotesStore struct {
|
type testMeetingNotesStore struct {
|
||||||
meetings []types.Meeting
|
meetings []types.Meeting
|
||||||
files []types.MeetingNoteFile
|
files []types.MeetingNoteFile
|
||||||
query string
|
query string
|
||||||
cursor string
|
cursor string
|
||||||
limit int
|
limit int
|
||||||
|
createdMeetingID string
|
||||||
|
createdFileID string
|
||||||
|
createdIdempotencyKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type testDailyReportsStore struct {
|
type testDailyReportsStore struct {
|
||||||
@@ -679,6 +682,9 @@ func (s *testMeetingNotesStore) ListFiles(_ types.TenantID, _ types.Uid, _ strin
|
|||||||
|
|
||||||
func (s *testMeetingNotesStore) CreateFile(_ types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error) {
|
func (s *testMeetingNotesStore) CreateFile(_ types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error) {
|
||||||
now := time.Now().UTC().Round(time.Millisecond)
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
|
s.createdMeetingID = meetingID
|
||||||
|
s.createdFileID = fileID
|
||||||
|
s.createdIdempotencyKey = idempotencyKey
|
||||||
return &types.MeetingNoteFile{
|
return &types.MeetingNoteFile{
|
||||||
NoteFileID: noteFileID, MeetingID: meetingID, FileID: fileID, UploaderID: uid,
|
NoteFileID: noteFileID, MeetingID: meetingID, FileID: fileID, UploaderID: uid,
|
||||||
FileName: fileID, Status: types.MeetingNoteFileStatusActive, PreviewStatus: "ready", PreviewMode: "native",
|
FileName: fileID, Status: types.MeetingNoteFileStatusActive, PreviewStatus: "ready", PreviewMode: "native",
|
||||||
|
|||||||
Reference in New Issue
Block a user