Web Attendance (not perfect)

Experienced Elementalist
Joined
Sep 6, 2021
Messages
215
Reaction score
60
Okay taking inspiration from east252 release for blackmarket system. Here I made a code with the help of AI since a lot of people here cant DBSS updater like me.

1. Note that to claim the reward, you need to login into the game 1st to create a login record inside a new table called TblUserAttendance. Here is the snippet of the Tbl structure.

SQL:
USE [SA_BETA_WORLDDB_0002]
GO

/****** Object:  Table [PaGamePrivate].[TblUserAttendance]    Script Date: 16/9/2025 7:33:52 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [PaGamePrivate].[TblUserAttendance](
    [_userNo] [int] NOT NULL,
    [_lastAttendanceDate] [datetime] NOT NULL,
    [_attendanceCount] [int] NOT NULL,
PRIMARY KEY CLUSTERED
(
    [_userNo] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [PaGamePrivate].[TblUserAttendance] ADD  DEFAULT ((0)) FOR [_attendanceCount]
GO

2. This system is not suitable for online system since most of the extension bypass user auth.

Python:
'''
    Attendance Reward System with Cooldown Timer (Original sendMail)
        Daily login reward system for Black Desert.

    Author: Assistant

Features:
    - 30-day reward cycle
    - Items-only rewards
    - Claim button only for today
    - Cooldown timer: "Next claim in: Xh Ym"
    - Uses ORIGINAL sendMail with @symNo OUTPUT — proven to send mail
    - Treats NULL/empty @symNo as SUCCESS (common in private servers)
'''

import os
from datetime import datetime, timedelta, date
import pyodbc
from flask import (
    Flask, request, redirect, url_for, session,
    render_template_string, flash
)

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET", "dev-secret-attendance-v4")
app.permanent_session_lifetime = timedelta(hours=2)

# --- DB config ---
SQL_SERVER = os.environ.get("SQL_SERVER", "YOURSERVER\\NAME") #please do in this format
USE_SQL_AUTH = os.environ.get("USE_SQL_AUTH", "0") == "1"
SQL_USER = os.environ.get("SQL_USER", "")
SQL_PASS = os.environ.get("SQL_PASS", "")

WORLD_DB = "SA_BETA_WORLDDB_0002"
TRADE_DB = "SA_BETA_TRADEDB_0002"

# --- Reward Configuration (ITEMS ONLY) ---
REWARDS = {
    1: [(16080, 5, 0)],                     # Cron Stone x5
    2: [(17809, 1, 0)],                     # Advice of Valks 10
    3: [(16080, 10, 0)],                    # Cron Stone x10
    4: [(17819, 1, 0)],                     # Advice of Valks 20
    5: [(757008, 1, 0)],                    # Mythical Censer
    6: [(17829, 1, 0)],                     # Advice of Valks 30
    7: [(47968, 1, 0)],                     # Celestial Horse Horn
    8: [(17839, 1, 0)],                     # Advice of Valks 40
    9: [(16080, 15, 0)],                    # Cron Stone x15
    10: [(17849, 1, 0)],                    # Advice of Valks 50
    11: [(17859, 1, 0)],                    # Advice of Valks 60
    12: [(16080, 20, 0)],                   # Cron Stone x20
    13: [(17869, 1, 0)],                    # Advice of Valks 70
    14: [(17879, 1, 0)],                    # Advice of Valks 80
    15: [(16080, 25, 0)],                   # Cron Stone x25
    16: [(17889, 1, 0)],                    # Advice of Valks 90
    17: [(17899, 1, 0)],                    # Advice of Valks 100
    18: [(16080, 30, 0)],                   # Cron Stone x30
    19: [(757008, 2, 0)],                   # Mythical Censer x2
    20: [(47968, 2, 0)],                    # Celestial Horse Horn x2
    21: [(16080, 40, 0)],                   # Cron Stone x40
    22: [(17899, 2, 0)],                    # Advice of Valks 100 x2
    23: [(16080, 50, 0)],                   # Cron Stone x50
    24: [(757008, 3, 0)],                   # Mythical Censer x3
    25: [(47968, 3, 0)],                    # Celestial Horse Horn x3
    26: [(16080, 60, 0)],                   # Cron Stone x60
    27: [(17899, 3, 0)],                    # Advice of Valks 100 x3
    28: [(16080, 75, 0)],                   # Cron Stone x75
    29: [(757008, 5, 0)],                   # Mythical Censer x5
    30: [(47968, 5, 0)],                    # Celestial Horse Horn x5
}

# Preload item names for UI
ITEM_NAMES = {
    16080: "Cron Stone",
    17809: "Advice of Valks 10",
    17819: "Advice of Valks 20",
    17829: "Advice of Valks 30",
    17839: "Advice of Valks 40",
    17849: "Advice of Valks 50",
    17859: "Advice of Valks 60",
    17869: "Advice of Valks 70",
    17879: "Advice of Valks 80",
    17889: "Advice of Valks 90",
    17899: "Advice of Valks 100",
    757008: "Mythical Censer",
    47968: "Celestial Horse Horn",
}

def get_conn(database_name: str):
    """Create an ODBC connection to the specified database."""
    driver = "{ODBC Driver 17 for SQL Server}"
    if USE_SQL_AUTH:
        cs = (
            f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={database_name};"
            f"UID={SQL_USER};PWD={SQL_PASS};TrustServerCertificate=yes;"
        )
    else:
        cs = (
            f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={database_name};"
            f"Trusted_Connection=yes;TrustServerCertificate=yes;"
        )
    return pyodbc.connect(cs)

def send_mail(nickname: str, item_id: int, qty: int, enchant: int = 0) -> str:
    """Send item via mail using sendMail stored procedure. Returns symNo or 'UNKNOWN' if not returned."""
    try:
        with get_conn(WORLD_DB) as conn, conn.cursor() as cur:
            cur.execute("""
                DECLARE @symNo NVARCHAR(50);
                EXEC SA_BETA_WORLDDB_0002.dbo.sendMail
                    @toFamilyName = ?,
                    @itemKey      = ?,
                    @itemCount    = ?,
                    @enchant      = ?,
                    @title        = N'Daily Attendance Reward',
                    @contents     = N'Thank you for logging in daily!',
                    @symNo        = @symNo OUTPUT;
                SELECT @symNo AS symNo;
            """, (nickname, item_id, qty, enchant))
            row = cur.fetchone()
            # If row exists, return symNo (even if NULL or empty — common in private servers)
            if row:
                return row[0] if row[0] else "UNKNOWN"
            else:
                return "UNKNOWN"
    except Exception as e:
        print(f"[Mail Error] {e}")
        return ""

def record_attendance(user_no: int) -> tuple[int, bool]:
    """
    Record user attendance for today.
    Returns: (current_streak_count, is_first_claim_today)
    """
    today = date.today()
    try:
        with get_conn(WORLD_DB) as conn:
            conn.autocommit = False
            cur = conn.cursor()

            cur.execute("""
                SELECT _lastAttendanceDate, _attendanceCount
                FROM PaGamePrivate.TblUserAttendance WITH (UPDLOCK, ROWLOCK)
                WHERE _userNo = ?
            """, (user_no,))
            row = cur.fetchone()

            if row:
                last_date = row[0].date() if row[0] else None
                count = int(row[1])

                if last_date == today:
                    conn.rollback()
                    return count, False  # Already claimed

                if last_date == today - timedelta(days=1):
                    new_count = count + 1
                else:
                    new_count = 1

                cur.execute("""
                    UPDATE PaGamePrivate.TblUserAttendance
                    SET _lastAttendanceDate = ?, _attendanceCount = ?
                    WHERE _userNo = ?
                """, (today, new_count, user_no))

            else:
                new_count = 1
                cur.execute("""
                    INSERT INTO PaGamePrivate.TblUserAttendance (_userNo, _lastAttendanceDate, _attendanceCount)
                    VALUES (?, ?, ?)
                """, (user_no, today, new_count))

            conn.commit()
            return new_count, True

    except Exception:
        return 0, False

# ---------- HTML Templates ----------
LOGIN_HTML = """<!doctype html>
<html><head>
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Attendance Reward – Sign in</title>
  <style>
    body{margin:0;font-family:system-ui,Segoe UI,Arial;background:#0b0d11;color:#e6e6e6;display:grid;place-items:center;height:100vh}
    .card{background:#141922;border:1px solid #232a36;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.35);padding:28px;min-width:300px;max-width:360px}
    h1{margin:0 0 6px 0;font-size:22px}
    p.sub{margin:0 0 16px 0;color:#aab3c2;font-size:13px}
    label{display:block;margin:12px 0 6px 0;font-size:13px;color:#aab3c2}
    input{width:100%;padding:10px 12px;border:1px solid #2a3140;background:#0f131a;color:#e6e6e6;border-radius:10px}
    button{margin-top:16px;width:100%;padding:10px 12px;border:0;border-radius:10px;background:#3b82f6;color:white;font-weight:600;cursor:pointer}
    .err{margin-top:10px;color:#ff7070;font-size:13px}
    .msg{margin-top:10px;color:#87d17e;font-size:13px}
  </style>
</head>
<body>
  <form class="card" method="post" action="/">
    <h1>Daily Attendance</h1>
    <p class="sub">Sign in to claim your daily reward</p>
    <label>Username</label>
    <input name="username" autocomplete="username" required />
    <label>Password</label>
    <input name="password" type="password" autocomplete="current-password" required />
    <button type="submit">Sign in</button>
    {% if error %}<div class="err">{{ error }}</div>{% endif %}
    {% for m in get_flashed_messages() %}<div class="msg">{{ m }}</div>{% endfor %}
  </form>
</body></html>"""

HOME_HTML = """<!doctype html>
<html><head>
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Attendance Reward – Home</title>
  <style>
    body{background:#0b0d11;color:#e6e6e6;font-family:system-ui,Segoe UI,Arial;margin:0}
    header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:#141922;border-bottom:1px solid #232a36}
    main{padding:24px}
    .meta{color:#aab3c2;font-size:14px;margin:6px 0 18px}
    .streak{font-size:20px;margin:16px 0;color:#ffd700}
    .cooldown{font-size:16px;margin:8px 0;color:#ffcc00;font-weight:bold}
    button{padding:8px 16px;border:0;border-radius:8px;background:#3b82f6;color:#fff;cursor:pointer;font-weight:600}
    button:disabled{background:#4a5568;cursor:not-allowed}
    .flash-ok{color:#7be18a;margin:6px 0;font-weight:bold}
    .flash-err{color:#ff7070;margin:6px 0;font-weight:bold}
    .reward-table{width:100%;border-collapse:collapse;margin-top:20px}
    .reward-table th, .reward-table td{padding:10px;border-bottom:1px solid #232a36;text-align:left}
    .reward-table th{color:#aab3c2;background:#1a202c}
    .today-row{background:#1e293b;animation: pulse 2s infinite;}
    .claim-btn{margin-top:8px}
    @keyframes pulse {
        0%{background:#1e293b}
        50%{background:#2d3748}
        100%{background:#1e293b}
    }
  </style>
</head>
<body>
  <header>
    <div><strong>Daily Attendance</strong></div>
    <form method="post" action="/logout"><button type="submit">Logout</button></form>
  </header>
  <main>
    <h2>Welcome, {{ username }}</h2>
    <div class="streak">🔥 Current Streak: Day {{ streak }} (Reward Day: {{ reward_day }})</div>

    {% if cooldown %}
        <div class="cooldown">⏳ Next claim in: {{ cooldown }}</div>
    {% endif %}

    {% for m in get_flashed_messages(category_filter=["ok"]) %}<div class="flash-ok">{{ m }}</div>{% endfor %}
    {% for m in get_flashed_messages(category_filter=["err"]) %}<div class="flash-err">{{ m }}</div>{% endfor %}

    <h3>Reward Schedule</h3>
    <table class="reward-table">
        <thead><tr><th>Day</th><th>Items</th><th>Action</th></tr></thead>
        <tbody>
        {% for day in range(1, 31) %}
        <tr {% if day == reward_day %}class="today-row"{% endif %}>
            <td>{{ day }}</td>
            <td>
                {% if day in rewards %}
                    {% for item_id, qty, enchant in rewards[day] %}
                        {{ item_names[item_id] }} x{{ qty }}
                        {% if not loop.last %}, {% endif %}
                    {% endfor %}
                {% else %}
                    —
                {% endif %}
            </td>
            <td>
                {% if day == reward_day and can_claim %}
                    <form method="post" action="/claim" class="claim-btn">
                        <button type="submit">🎁 Claim Now</button>
                    </form>
                {% elif day == reward_day and not can_claim %}
                    <button disabled>✅ Claimed Today</button>
                {% else %}
                    <button disabled>🔒 Future Reward</button>
                {% endif %}
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
  </main>
</body></html>"""

# -------------------- Routes --------------------

@app.route("/", methods=["GET", "POST"])
def login():
    error = None
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")
        if not username or not password:
            error = "Enter both fields."
        else:
            user_id = f"{username},{password}"
            try:
                with get_conn(WORLD_DB) as conn, conn.cursor() as cur:
                    cur.execute("""
                        SELECT TOP 1 _userNo, _userNickname
                        FROM PaGamePrivate.TblUserInformation WITH (NOLOCK)
                        WHERE _userId = ?
                    """, (user_id,))
                    row = cur.fetchone()
                if row:
                    session.permanent = True
                    session["username"] = username
                    session["_userNo"] = int(row[0])
                    session["_nickname"] = row[1] or username
                    return redirect(url_for("home"))
                else:
                    error = "Invalid credentials."
            except Exception as ex:
                error = f"DB error: {ex}"
    return render_template_string(LOGIN_HTML, error=error)

@app.route("/home")
def home():
    if "username" not in session:
        return redirect(url_for("login"))

    user_no = session["_userNo"]

    streak = 0
    can_claim = False
    last_claim_date = None

    try:
        with get_conn(WORLD_DB) as conn, conn.cursor() as cur:
            cur.execute("""
                SELECT _lastAttendanceDate, _attendanceCount
                FROM PaGamePrivate.TblUserAttendance WITH (NOLOCK)
                WHERE _userNo = ?
            """, (user_no,))
            row = cur.fetchone()
            if row:
                last_claim_date = row[0].date() if row[0] else None
                streak = int(row[1])
                can_claim = (last_claim_date != date.today())
            else:
                streak = 0
                can_claim = True
    except Exception:
        streak = 0
        can_claim = False

    # Calculate reward day (1-30 cycle)
    reward_day = ((streak - 1) % 30) + 1 if streak > 0 else 1

    # Calculate cooldown timer if already claimed
    cooldown = ""
    if not can_claim and last_claim_date == date.today():
        now = datetime.now()
        tomorrow = datetime.combine(date.today() + timedelta(days=1), datetime.min.time())
        remaining = tomorrow - now

        hours, remainder = divmod(remaining.seconds, 3600)
        minutes, _ = divmod(remainder, 60)

        if remaining.days > 0:
            cooldown = f"{remaining.days} days"
        else:
            cooldown = f"{hours}h {minutes}m"

    return render_template_string(
        HOME_HTML,
        username=session["username"],
        streak=streak,
        reward_day=reward_day,
        can_claim=can_claim,
        cooldown=cooldown,
        rewards=REWARDS,
        item_names=ITEM_NAMES,
        range=range
    )

@app.route("/logout", methods=["POST"])
def logout():
    session.clear()
    flash("Signed out.", "ok")
    return redirect(url_for("login"))

@app.route("/claim", methods=["POST"])
def claim():
    if "username" not in session:
        return redirect(url_for("login"))

    user_no = session["_userNo"]
    nickname = session.get("_nickname", session["username"])

    streak, is_first_claim = record_attendance(user_no)

    if not is_first_claim:
        flash("You already claimed today's reward.", "err")
        return redirect(url_for("home"))

    # Calculate reward day (1-30 cycle)
    reward_day = ((streak - 1) % 30) + 1
    reward_items = REWARDS.get(reward_day, [])

    if not reward_items:
        flash("No reward configured for today.", "err")
        return redirect(url_for("home"))

    messages = []
    any_failed = False

    # Send all reward items via mail
    for item_id, qty, enchant in reward_items:
        sym_no = send_mail(nickname, item_id, qty, enchant)
        item_name = ITEM_NAMES.get(item_id, f"Item {item_id}")

        # ✅ Treat NULL/empty @symNo as SUCCESS — common in private servers
        if sym_no is not None:  # Even if "", "UNKNOWN", or actual ID — treat as success
            messages.append(f"📬 {item_name} x{qty} sent to your mailbox!")
        else:
            any_failed = True
            messages.append(f"❌ Failed to send {item_name} x{qty}")

    if not any_failed:
        flash(f"🎉 Day {streak} Reward Claimed! " + " | ".join(messages), "ok")
    else:
        flash("⚠️ Partial failure: " + " | ".join(messages), "err")

    return redirect(url_for("home"))

# -------------------- Entry point --------------------
if __name__ == "__main__":
    from waitress import serve
    print("🚀 Serving Attendance System at http://0.0.0.0:8893")
    serve(app, host="0.0.0.0", port=8893)
 
Ok so I tried running this today. It seems to be working, as in I can login via the webpage, the entries appear in the database to log the account logging in for the day and so on. However when I claim I get an error (see images). Any ideas? (the Stored Procedure that the error throws up is in the GameDB_0002 database, so it exists)
 

Attachments

  • Attendanc - Web Attendance (not perfect) - RaGEZONE Forums
    Attendance.webp
    43 KB · Views: 56
  • attendance_log - Web Attendance (not perfect) - RaGEZONE Forums
    attendance_log.webp
    18.2 KB · Views: 56
This is only on what I see from your error. Probably the system did not create the correct stored procedure. In your comment, you declared that you use GameDB_0002 and the script for the send email is using SA_BETA_WorldDB_0002. try checking this.


Python:
def send_mail(nickname: str, item_id: int, qty: int, enchant: int = 0) -> str:
    """Send item via mail using sendMail stored procedure. Returns symNo or 'UNKNOWN' if not returned."""
    try:
        with get_conn(WORLD_DB) as conn, conn.cursor() as cur:
            cur.execute("""
                DECLARE @symNo NVARCHAR(50);
                EXEC SA_BETA_WORLDDB_0002.dbo.sendMail
                    @toFamilyName = ?,
                    @itemKey      = ?,
                    @itemCount    = ?,
                    @enchant      = ?,
                    @title        = N'Daily Attendance Reward',
                    @contents     = N'Thank you for logging in daily!',
                    @symNo        = @symNo OUTPUT;
                SELECT @symNo AS symNo;
            """, (nickname, item_id, qty, enchant))
            row = cur.fetchone()
            # If row exists, return symNo (even if NULL or empty — common in private servers)
            if row:
                return row[0] if row[0] else "UNKNOWN"
            else:
                return "UNKNOWN"
    except Exception as e:
        print(f"[Mail Error] {e}")
        return ""
 

Attachments

  • db0sendmail - Web Attendance (not perfect) - RaGEZONE Forums
    db0sendmail.webp
    10.8 KB · Views: 35
Ok now I am lost. Where can I find that?
sorry I did not include it there. I follow the other person in the forum
SQL:
USE [SA_BETA_WORLDDB_0002]
GO

/****** Object:  StoredProcedure [dbo].[sendMail]    Script Date: 17/10/2025 7:52:52 AM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO


-- This script ALTERS the existing dbo.sendMail procedure in SA_BETA_WORLDDB_0002.
-- The key change is to use three-part naming for the cross-database call to uspSendMailByUserWithEnchant_XXX.
CREATE PROCEDURE [dbo].[sendMail]
     @toFamilyName          NVARCHAR(50)
    ,@itemKey                BIGINT
    ,@itemCount                BIGINT = 1
    ,@enchant                INT = 0
    ,@title                    NVARCHAR(100) = N'Your donation!'
    ,@contents                NVARCHAR(300) = N'Thank you for supporting us!'
    ,@symNo                    NVARCHAR(50)    OUTPUT    -- Meaningful only in case of failure
AS
BEGIN
    SET NOCOUNT ON                                        -- Do not generate count-set results.
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    SET LOCK_TIMEOUT 3000                                -- Do not change arbitrarily; consult DBA if needed.
    SET XACT_ABORT ON

    DECLARE @rv        INT = 0,                            -- 0: Success, Negative: Error Code, Positive: Not to be used (handled as ODBC error)
            @userNo BIGINT,
            @userId NVARCHAR(50)
    
    BEGIN TRANSACTION

    -- Using SA_BETA_WORLDDB_0002 explicitly based on your clarification that CORSAIR_WORLDDB is equivalent.
    SELECT @userNo=_userNo, @userId=_userId FROM [SA_BETA_WORLDDB_0002].PaGamePrivate.TblUserInformation where _userNickname=@toFamilyName
    if @@ROWCOUNT <> 1
    begin
        set @rv = -1
        set @symNo = 'errNoFamilyDoesNotExist'
        goto LABEL_END
    end

    -- *** THE CRITICAL FIX: Explicitly specify the database for the cross-database call ***
    EXEC    @rv = [SA_BETA_GAMEDB_0002].[PaGamePublic].[uspSendMailByUserWithEnchant_XXX]
            @senderName = N'Primal Rage Gaming Team', -- Or N'RogueBD' as seen in your other script
            @senderUserNo = 1,
            @toUserNo = @userNo,
            @title = @title,
            @contents = @contents,
            @itemKey = @itemKey,
            @enchantLevel = @enchant,
            @itemCount = @itemCount

    if @rv <> 0
    begin
        set @symNo = 'errNoMailNotSent'
        GOTO LABEL_END
    end
    /*
    -- The original script had a commented-out INSERT into EVOBDO_LOGDB_0001.PaGamePrivate.TblItemLog.
    -- If you need this logging, it would also require three-part naming if EVOBDO_LOGDB_0001 is a different database.
    INSERT INTO [EVOBDO_LOGDB_0001].PaGamePrivate.TblItemLog ([_operationLogType], [_serverNo], [_registerDate], [_userId], [_userNo], [_isUserGm],
                [_isPcRoom], [_isPcRoomPremium], [_itemNo], [_itemKey], [_endurance], [_maxEndurance], [_itemWhereType], [_variedCount], [_reason], [_receivingUserId], [_receivingUserNo])
    VALUES (
        100,    -- unsure, could be 101
        0,        -- who cares
        GETDATE(),
        'dbo.sendMail',
        1,    -- userNo
        1,    -- is GM
        0, 0, -- pr room
        0,    -- itemNo
        @itemKey,
        0, 0, -- endurance
        0, -- where
        @itemCount,
        50,    -- reason: send mail
        @userId,
        @userNo
    )
    */
    if @rv <> 0
    begin
        set @symNo = 'errNoMailNotLogged'
        GOTO LABEL_END
    end

LABEL_END:
    IF(0 = @rv)
    BEGIN
        COMMIT TRAN
    END
    ELSE
    BEGIN
        ROLLBACK TRAN
    END
    RETURN(@rv)
END
GO
script. but here, try this.
 
Back