add routers and logic for online donat
This commit is contained in:
parent
a4cbb43ccd
commit
ae5301a222
@ -713,6 +713,40 @@ func GetDonatForPlaying(donatService model.DonatService) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStreamerOnline godoc
|
||||
// @Summary Update streamer online status
|
||||
// @Description Marks streamer as online in the system
|
||||
// @Tags Donate
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param streamer-id path string true "Streamer ID (numeric)"
|
||||
// @Success 200 {object} map[string]interface{} "Success status"
|
||||
// @Failure 400 {object} echo.HTTPError "Invalid streamer ID"
|
||||
// @Failure 500 {object} echo.HTTPError "Internal server error"
|
||||
// @Router /update-streamer-online/{streamer-id} [put]
|
||||
func UpdateStreamerOnline(donatService model.DonatService) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
streamerIDParam := c.Param("streamer-id")
|
||||
|
||||
streamerID, err := strconv.Atoi(streamerIDParam)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid streamer ID format")
|
||||
}
|
||||
|
||||
err = donatService.UpdateStreamerOnline(ctx, streamerID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to update streamer online status", "error", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update online status")
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "streamer online status updated",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLoginDonatePage godoc
|
||||
// @Summary Update streamer login
|
||||
// @Description Updates the streamer login associated with the donate page
|
||||
|
@ -107,6 +107,7 @@ func IncludeDonatHandlers(
|
||||
server.GET(PREFIX+"/get-donat-for-playing/:streamer-id", GetDonatForPlaying(donatService))
|
||||
server.GET(PREFIX+"/text-after-donat/:order-id", GetMessageAfterDonat(donatService))
|
||||
server.POST(PREFIX+"/update-login-donate", UpdateLoginDonatePage(donatService))
|
||||
server.PUT(PREFIX+"/update-streamer-online/:streamer-id", UpdateStreamerOnline(donatService))
|
||||
}
|
||||
|
||||
func IncludeWidgetHandlers(
|
||||
|
@ -1201,6 +1201,51 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/update-streamer-online/{streamer-id}": {
|
||||
"put": {
|
||||
"description": "Marks streamer as online in the system",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Donate"
|
||||
],
|
||||
"summary": "Update streamer online status",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Streamer ID (numeric)",
|
||||
"name": "streamer-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success status",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid streamer ID",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/echo.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/echo.HTTPError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/voice-settings": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1194,6 +1194,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/update-streamer-online/{streamer-id}": {
|
||||
"put": {
|
||||
"description": "Marks streamer as online in the system",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Donate"
|
||||
],
|
||||
"summary": "Update streamer online status",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Streamer ID (numeric)",
|
||||
"name": "streamer-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success status",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid streamer ID",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/echo.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/echo.HTTPError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/voice-settings": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1328,6 +1328,36 @@ paths:
|
||||
summary: Update streamer login
|
||||
tags:
|
||||
- Donate
|
||||
/update-streamer-online/{streamer-id}:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Marks streamer as online in the system
|
||||
parameters:
|
||||
- description: Streamer ID (numeric)
|
||||
in: path
|
||||
name: streamer-id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success status
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid streamer ID
|
||||
schema:
|
||||
$ref: '#/definitions/echo.HTTPError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/echo.HTTPError'
|
||||
summary: Update streamer online status
|
||||
tags:
|
||||
- Donate
|
||||
/voice-settings:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"donat-widget/internal/model/api"
|
||||
"github.com/google/uuid"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
)
|
||||
|
||||
import (
|
||||
@ -108,6 +109,7 @@ type DonatService interface {
|
||||
UpdateStreamerLogin(ctx context.Context, streamerLogin string, streamerID int) error
|
||||
UpdateAvatarStreamer(token, avatarId string) error
|
||||
GetTextAfterDonatByOrder(ctx context.Context, orderID uuid.UUID) (string, error)
|
||||
UpdateStreamerOnline(ctx context.Context, streamerID int) error
|
||||
}
|
||||
|
||||
type DonatRepo interface {
|
||||
@ -178,6 +180,8 @@ type DonatRepo interface {
|
||||
|
||||
GetPlayingDonat(ctx context.Context, streamerID int) (PlayingDonat, error)
|
||||
UpdateStreamerLogin(ctx context.Context, streamerLogin string, streamerID int) error
|
||||
CreateStreamerOnline(ctx context.Context, streamerID int) error
|
||||
GetLastStreamerOnline(ctx context.Context, streamerID int) (time.Time, error)
|
||||
}
|
||||
|
||||
type TargetService interface {
|
||||
|
@ -130,6 +130,12 @@ CREATE TABLE IF NOT EXISTS streamers_widgets_pages (
|
||||
id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
||||
streamer_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS streamers_online (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
streamer_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
);
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
|
@ -589,3 +589,9 @@ SELECT streamer_id FROM streamers_widgets_pages WHERE id = @widget_page_id`
|
||||
var UpdateStreamerLogin = `
|
||||
UPDATE donate_pages SET streamer_login = @streamer_login WHERE streamer_id = @streamer_id
|
||||
RETURNING streamer_login;`
|
||||
|
||||
var InsertStreamerOnline = `
|
||||
INSERT INTO streamers_online (streamer_id) VALUES (@streamer_id)`
|
||||
|
||||
var GetLastStreamerOnline = `
|
||||
SELECT created_at FROM streamers_online WHERE streamer_id = @streamer_id`
|
||||
|
@ -64,6 +64,20 @@ func GetTemplate1(streamerID int, donatHost, ttsHost string) string {
|
||||
let widgetUrl = 'https://%s/api';
|
||||
let ttsUrl = 'https://%s/api/tts';
|
||||
|
||||
// Функция для периодических запросов онлайн-статуса
|
||||
function startHeartbeat(streamerID) {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await fetch(widgetUrl + '/update-streamer-online/' + streamerID, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Heartbeat error:', error);
|
||||
}
|
||||
}, 10000); // 10 секунд
|
||||
}
|
||||
|
||||
function createTextWithAmount(text, amount) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'text-container';
|
||||
@ -120,7 +134,7 @@ function playAudio(url, volume, signal) {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
audio.pause();
|
||||
audio.src = ""; // Release resources
|
||||
audio.src = "";
|
||||
audio.removeEventListener('ended', onEnded);
|
||||
audio.removeEventListener('error', onError);
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
@ -148,16 +162,15 @@ function playAudio(url, volume, signal) {
|
||||
audio.addEventListener('error', onError, { once: true });
|
||||
|
||||
audio.play().catch(err => {
|
||||
// play() can reject if interrupted or no user gesture
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
audio.removeEventListener('ended', onEnded);
|
||||
audio.removeEventListener('error', onError);
|
||||
if (err.name === 'AbortError' && signal?.aborted) {
|
||||
reject(new DOMException('Aborted', 'AbortError')); // Propagate abort
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
} else {
|
||||
reject(err); // Other play() error
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -208,7 +221,7 @@ function playSpeech(text, voiceSettings, signal) {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: signal // Pass signal to fetch
|
||||
signal: signal
|
||||
})
|
||||
.then(response => {
|
||||
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
|
||||
@ -241,10 +254,9 @@ function playSpeech(text, voiceSettings, signal) {
|
||||
return audio.play();
|
||||
})
|
||||
.catch(error => {
|
||||
// This catches errors from fetch, blob processing, or audio.play()
|
||||
if (resolved) return; // Already handled
|
||||
signal?.removeEventListener('abort', onAbort); // Ensure listener removed
|
||||
cleanupAndReject(error); // Pass the original error
|
||||
if (resolved) return;
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
cleanupAndReject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -273,26 +285,24 @@ async function playMedia(donat, voiceSettings) {
|
||||
ttsAudio = await playSpeech(donat.text, voiceSettings, controller.signal);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') { // Only re-throw non-abort errors
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Error during media playback:', error);
|
||||
throw error;
|
||||
}
|
||||
// If AbortError, it's handled by the timeout logic or external abort.
|
||||
}
|
||||
})();
|
||||
|
||||
const timeoutPromise = new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
controller.abort(); // Signal all operations to stop
|
||||
resolve('timeout'); // Resolve to indicate timeout completion
|
||||
}, (donat.duration || 0) * 1000); // Use donat.duration, default to 0 if undefined
|
||||
controller.abort();
|
||||
resolve('timeout');
|
||||
}, (donat.duration || 0) * 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([mediaOperation, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId); // Crucial: clear timeout if mediaOperation finished first or errored
|
||||
// Ensure audio elements are stopped and reset if they were initiated and not properly cleaned up by abort
|
||||
clearTimeout(timeoutId);
|
||||
if (audioElement && !audioElement.paused) {
|
||||
audioElement.pause();
|
||||
audioElement.currentTime = 0;
|
||||
@ -312,6 +322,8 @@ function clearContainer(container) {
|
||||
|
||||
async function widgetView() {
|
||||
const streamerID = '%v';
|
||||
startHeartbeat(streamerID); // Запускаем heartbeat
|
||||
|
||||
const contentDiv = document.getElementById('content');
|
||||
|
||||
if (!contentDiv) {
|
||||
@ -321,15 +333,15 @@ async function widgetView() {
|
||||
|
||||
while (true) {
|
||||
const iterationStart = Date.now();
|
||||
let currentDonat = null; // Store current donat for duration calculation
|
||||
let currentDonat = null;
|
||||
|
||||
try {
|
||||
const donatData = await getDonatInfo(streamerID);
|
||||
if (!donatData || Object.keys(donatData).length === 0) {
|
||||
await new Promise(r => setTimeout(r, 5000)); // Wait before retrying
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
continue;
|
||||
}
|
||||
currentDonat = donatData; // Assign to currentDonat
|
||||
currentDonat = donatData;
|
||||
|
||||
clearContainer(contentDiv);
|
||||
|
||||
@ -363,7 +375,6 @@ async function widgetView() {
|
||||
voice_enabled: currentDonat.voice_enabled
|
||||
};
|
||||
|
||||
// playMedia will ensure audio plays for at most currentDonat.duration
|
||||
await playMedia(currentDonat, voiceSettings);
|
||||
|
||||
if (currentDonat.order_id) {
|
||||
@ -379,18 +390,14 @@ async function widgetView() {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Log errors from getDonatInfo, playMedia, or other processing
|
||||
console.error('Ошибка обработки доната в цикле:', error);
|
||||
// If error, maybe wait a bit before next iteration to avoid spamming
|
||||
if (!currentDonat) { // e.g. error in getDonatInfo
|
||||
if (!currentDonat) {
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - iterationStart;
|
||||
// Total display time for the donation (visuals + audio)
|
||||
// Use currentDonat.duration if available, otherwise default (e.g., 5 seconds)
|
||||
const targetDisplayTimeMs = (currentDonat?.duration || 5) * 1000;
|
||||
|
||||
const remainingTimeMs = Math.max(0, targetDisplayTimeMs - elapsedMs);
|
||||
@ -398,7 +405,6 @@ async function widgetView() {
|
||||
if (remainingTimeMs > 0) {
|
||||
await new Promise(r => setTimeout(r, remainingTimeMs));
|
||||
}
|
||||
// If elapsedMs already exceeds targetDisplayTimeMs, loop will continue immediately
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -881,3 +881,36 @@ func (repoDonat *RepoDonat) UpdateStreamerLogin(
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repoDonat *RepoDonat) CreateStreamerOnline(
|
||||
ctx context.Context,
|
||||
streamerID int,
|
||||
) error {
|
||||
args := pgx.NamedArgs{
|
||||
"streamer_id": streamerID,
|
||||
}
|
||||
err := repoDonat.db.Exec(ctx, sql.InsertStreamerOnline, args)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repoDonat *RepoDonat) GetLastStreamerOnline(
|
||||
ctx context.Context,
|
||||
streamerID int,
|
||||
) (time.Time, error) {
|
||||
args := pgx.NamedArgs{
|
||||
"streamer_id": streamerID,
|
||||
}
|
||||
row := repoDonat.db.SelectOne(ctx, sql.GetLastStreamerOnline, args)
|
||||
|
||||
var StreamerOnline time.Time
|
||||
|
||||
err := row.Scan(&StreamerOnline)
|
||||
if err != nil {
|
||||
return StreamerOnline, err
|
||||
}
|
||||
return StreamerOnline, nil
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServiceDonat struct {
|
||||
@ -343,10 +344,18 @@ func (donatService *ServiceDonat) GetOuterDonatPage(
|
||||
}
|
||||
}
|
||||
|
||||
err = donatService.getLastStreamerOnline(ctx, donatePage.StreamerID)
|
||||
var online string
|
||||
if err != nil {
|
||||
online = "offline"
|
||||
} else {
|
||||
online = "online"
|
||||
}
|
||||
|
||||
var outerDonatePageResponse = model.OuterDonatePageResponse{
|
||||
Description: donatePage.Description,
|
||||
Login: donatePage.StreamerLogin,
|
||||
OnLine: "online",
|
||||
OnLine: online,
|
||||
BackgroundImg: donatService.storage.DownloadLink(donatePage.BackgroundImgFileId),
|
||||
HeadImg: donatService.storage.DownloadLink(donatePage.HeadImgFileId),
|
||||
AvatarImg: donatService.storage.DownloadLink(avatarFileId),
|
||||
@ -856,6 +865,46 @@ func (donatService *ServiceDonat) replaceFilteredWords(text string, words []stri
|
||||
})
|
||||
}
|
||||
|
||||
func (donatService *ServiceDonat) UpdateStreamerOnline(
|
||||
ctx context.Context,
|
||||
streamerID int,
|
||||
) error {
|
||||
err := donatService.donatRepo.CreateStreamerOnline(
|
||||
ctx,
|
||||
streamerID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (donatService *ServiceDonat) getLastStreamerOnline(
|
||||
ctx context.Context,
|
||||
streamerID int,
|
||||
) error {
|
||||
onlineTime, err := donatService.donatRepo.GetLastStreamerOnline(
|
||||
ctx,
|
||||
streamerID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
diff := currentTime.Sub(onlineTime)
|
||||
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
|
||||
if diff > time.Minute {
|
||||
return fmt.Errorf("last online time is more than a minute old")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (donatService *ServiceDonat) replaceLinks(text string) string {
|
||||
re := regexp.MustCompile(`(?i)\bhttps?://\S+\b`)
|
||||
return re.ReplaceAllStringFunc(text, func(match string) string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user