• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

u-wave / core / 11980840475

22 Nov 2024 10:04PM UTC coverage: 78.492% (-1.7%) from 80.158%
11980840475

Pull #637

github

goto-bus-stop
ci: add node 22
Pull Request #637: Switch to a relational database

757 of 912 branches covered (83.0%)

Branch coverage included in aggregate %.

2001 of 2791 new or added lines in 52 files covered. (71.69%)

9 existing lines in 7 files now uncovered.

8666 of 11093 relevant lines covered (78.12%)

70.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.74
/src/plugins/bans.js
1
import lodash from 'lodash';
1✔
2
import { UserNotFoundError } from '../errors/index.js';
1✔
3
import Page from '../Page.js';
1✔
4
import { now } from '../utils/sqlite.js';
1✔
5

1✔
6
const { clamp } = lodash;
1✔
7

1✔
8
class Bans {
92✔
9
  #uw;
92✔
10

92✔
11
  /**
92✔
12
   * @param {import('../Uwave.js').default} uw
92✔
13
   */
92✔
14
  constructor(uw) {
92✔
15
    this.#uw = uw;
92✔
16
  }
92✔
17

92✔
18
  /**
92✔
19
   * Check whether a user is currently banned.
92✔
20
   *
92✔
21
   * @param {import('../schema.js').User} user A user object.
92✔
22
   */
92✔
23

92✔
24
  async isBanned(user) {
92✔
25
    const { db } = this.#uw;
127✔
26

127✔
27
    const ban = await db.selectFrom('bans')
127✔
28
      .selectAll()
127✔
29
      .where('userID', '=', user.id)
127✔
30
      .where(({ or, eb }) => or([
127✔
31
        eb('expiresAt', 'is', null),
127✔
32
        eb('expiresAt', '>', now),
127✔
33
      ]))
127✔
34
      .executeTakeFirst();
127✔
35

127✔
36
    return ban != null;
127✔
37
  }
127✔
38

92✔
39
  /**
92✔
40
   * List banned users.
92✔
41
   *
92✔
42
   * @param {string} [filter] Optional filter to search for usernames.
92✔
43
   * @param {{ offset?: number, limit?: number }} [pagination] A pagination object.
92✔
44
   */
92✔
45
  async getBans(filter, pagination = {}) {
92✔
46
    const { db } = this.#uw;
2✔
47

2✔
48
    const offset = pagination.offset ?? 0;
2!
49
    const size = clamp(
2✔
50
      pagination.limit == null ? 50 : pagination.limit,
2!
51
      0,
2✔
52
      100,
2✔
53
    );
2✔
54

2✔
55
    let query = db.selectFrom('bans')
2✔
56
      .innerJoin('users', 'users.id', 'bans.userID')
2✔
57
      .leftJoin('users as mod', 'mod.id', 'bans.moderatorID')
2✔
58
      .select([
2✔
59
        'users.id as users.id',
2✔
60
        'users.username as users.username',
2✔
61
        'users.slug as users.slug',
2✔
62
        'users.createdAt as users.createdAt',
2✔
63
        'users.updatedAt as users.updatedAt',
2✔
64
        'mod.id as mod.id',
2✔
65
        'mod.username as mod.username',
2✔
66
        'mod.slug as mod.slug',
2✔
67
        'mod.createdAt as mod.createdAt',
2✔
68
        'mod.updatedAt as mod.updatedAt',
2✔
69
        'bans.reason',
2✔
70
        'bans.expiresAt',
2✔
71
        'bans.createdAt',
2✔
72
      ])
2✔
73
      .where(({ eb, or }) => or([
2✔
74
        eb('expiresAt', 'is', null),
2✔
75
        eb('expiresAt', '>', now),
2✔
76
      ]));
2✔
77

2✔
78
    if (filter) {
2!
NEW
79
      query = query.where('users.username', 'like', filter);
×
80
    }
×
81

2✔
82
    const { total } = await db.selectFrom('bans').select(eb => eb.fn.countAll().as('total')).executeTakeFirstOrThrow();
2✔
83
    const { filtered } = await query.select(eb => eb.fn.countAll().as('filtered')).executeTakeFirstOrThrow();
2✔
84

2✔
85
    query = query.offset(offset).limit(size);
2✔
86

2✔
87
    const bannedUsers = await query.execute();
2✔
88
    const results = bannedUsers.map((row) => ({
2✔
89
      user: {
1✔
90
        id: row['users.id'],
1✔
91
        username: row['users.username'],
1✔
92
        slug: row['users.slug'],
1✔
93
        createdAt: row['users.createdAt'],
1✔
94
        updatedAt: row['users.updatedAt'],
1✔
95
      },
1✔
96
      moderator: row['mod.id'] != null ? {
1✔
97
        id: row['mod.id'],
1✔
98
        username: row['mod.username'],
1✔
99
        slug: row['mod.slug'],
1✔
100
        createdAt: row['mod.createdAt'],
1✔
101
        updatedAt: row['mod.updatedAt'],
1✔
102
      } : null,
1!
103
      reason: row.reason,
1✔
104
      duration: row.expiresAt != null
1✔
105
        ? Math.floor(row.expiresAt.getTime() / 1_000 - row.createdAt.getTime() / 1_000) * 1_000
1✔
106
        : 0,
1!
107
      expiresAt: row.expiresAt,
1✔
108
      createdAt: row.createdAt,
1✔
109
    }));
2✔
110

2✔
111
    return new Page(results, {
2✔
112
      pageSize: pagination ? pagination.limit : undefined,
2!
113
      filtered: Number(filtered),
2✔
114
      total: Number(total),
2✔
115
      current: { offset, limit: size },
2✔
116
      next: pagination ? { offset: offset + size, limit: size } : undefined,
2!
117
      previous: offset > 0
2✔
118
        ? { offset: Math.max(offset - size, 0), limit: size }
2!
119
        : null,
2✔
120
    });
2✔
121
  }
2✔
122

92✔
123
  /**
92✔
124
   * @param {import('../schema.js').User} user
92✔
125
   * @param {object} options
92✔
126
   * @param {number} options.duration
92✔
127
   * @param {import('../schema.js').User} options.moderator
92✔
128
   * @param {boolean} [options.permanent]
92✔
129
   * @param {string} [options.reason]
92✔
130
   */
92✔
131
  async ban(user, {
92✔
132
    duration, moderator, permanent = false, reason = '',
3✔
133
  }) {
3✔
134
    const { db } = this.#uw;
3✔
135

3✔
136
    if (duration <= 0 && !permanent) {
3!
137
      throw new Error('Ban duration should be at least 0ms.');
×
138
    }
×
139

3✔
140
    const createdAt = new Date(Math.floor(Date.now() / 1_000) * 1_000);
3✔
141
    const expiresAt = permanent ? null : new Date(createdAt.getTime() + duration);
3✔
142
    const ban = {
3✔
143
      userID: user.id,
3✔
144
      moderatorID: moderator.id,
3✔
145
      createdAt,
3✔
146
      expiresAt,
3✔
147
      reason: reason || null,
3✔
148
    };
3✔
149

3✔
150
    await db.insertInto('bans')
3✔
151
      .values(ban)
3✔
152
      .executeTakeFirstOrThrow();
3✔
153

3✔
154
    this.#uw.publish('user:ban', {
3✔
155
      userID: user.id,
3✔
156
      moderatorID: moderator.id,
3✔
157
      duration,
3✔
158
      expiresAt: ban.expiresAt ? ban.expiresAt.getTime() : null,
3✔
159
      permanent,
3✔
160
    });
3✔
161

3✔
162
    return ban;
3✔
163
  }
3✔
164

92✔
165
  /**
92✔
166
   * @param {import('../schema.js').UserID} userID
92✔
167
   * @param {object} options
92✔
168
   * @param {import('../schema.js').User} options.moderator
92✔
169
   */
92✔
170
  async unban(userID, { moderator }) {
92✔
171
    const { db, users } = this.#uw;
1✔
172

1✔
173
    const user = await users.getUser(userID);
1✔
174
    if (!user) {
1!
175
      throw new UserNotFoundError({ id: userID });
×
176
    }
×
177

1✔
178
    const result = await db.deleteFrom('bans')
1✔
179
      .where('userID', '=', userID)
1✔
180
      .executeTakeFirst();
1✔
181
    if (result.numDeletedRows === 0n) {
1!
182
      throw new Error(`User "${user.username}" is not banned.`);
×
183
    }
×
184

1✔
185
    this.#uw.publish('user:unban', {
1✔
186
      userID,
1✔
187
      moderatorID: moderator.id,
1✔
188
    });
1✔
189
  }
1✔
190
}
92✔
191

1✔
192
/**
1✔
193
 * @param {import('../Uwave.js').default} uw
1✔
194
 */
1✔
195
async function bans(uw) {
92✔
196
  uw.bans = new Bans(uw);
92✔
197
}
92✔
198

1✔
199
export default bans;
1✔
200
export { Bans };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc