Skip to content

Commit

Permalink
Added streams watched view
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaserlang committed Dec 2, 2022
1 parent e79d85a commit 60ae29a
Show file tree
Hide file tree
Showing 10 changed files with 641 additions and 195 deletions.
392 changes: 305 additions & 87 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions tbot/migrations/20221202_01_mHmlh-twitch-stream-watchtime-index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
twitch_stream_watchtime index
"""

from yoyo import step

__depends__ = {'20221128_01_rQWlg-twitch-scope-length'}

steps = [
step('''
ALTER TABLE twitch_stream_watchtime
DROP PRIMARY KEY,
ADD PRIMARY KEY (channel_id, user_id, stream_id);
''')
]
1 change: 0 additions & 1 deletion tbot/utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def isoformat(dt):
return r



class JsonEncoder(json.JSONEncoder):
def default(self, value):
"""Convert more Python data types to ES-understandable JSON."""
Expand Down
1 change: 1 addition & 0 deletions tbot/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def App():
(r'/api/twitch/channels', handlers.api.twitch.channels.Handler),
(r'/api/twitch/channels/([0-9]+)/chatlog', handlers.api.twitch.chatlog.Handler),
(r'/api/twitch/channels/([0-9]+)/user-chatstats', handlers.api.twitch.chatlog.User_stats_handler),
(r'/api/twitch/channels/([0-9]+)/user-streams-watched', handlers.api.twitch.chatlog.User_streams_watched_handler),
(r'/api/twitch/channels/([0-9]+)/users', handlers.api.twitch.channel_users.Handler),
(r'/api/twitch/channels/([0-9]+)/bot-join', handlers.api.twitch.control_bot.Join_handler),
(r'/api/twitch/channels/([0-9]+)/bot-mute', handlers.api.twitch.control_bot.Mute_handler),
Expand Down
86 changes: 65 additions & 21 deletions tbot/web/handlers/api/twitch/chatlog.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import logging
from tornado import web
from ..base import Api_handler
from dateutil.parser import parse


class Handler(Api_handler):

async def get(self, channel_id):
# TODO: Add setting to allow or disallow non mods to view the logs
# For the moment only mods are allowed to view them
if True:
if not self.current_user:
raise web.HTTPError(401, 'Authentication required')
mod = await self.db.fetchone('SELECT user_id FROM twitch_channel_mods WHERE channel_id=%s AND user_id=%s',
[channel_id, self.current_user['user_id']])
if not mod:
raise web.HTTPError(403, 'You are not a moderator of this channel')
await has_mod(self, channel_id)

args = [channel_id]
sql = 'SELECT * FROM twitch_chatlog WHERE channel_id=%s AND type IN (1,100) '

user = self.get_argument('user', None)
if user:
u = await self.db.fetchone('SELECT user_id FROM twitch_usernames WHERE user=%s', [user])
user_id = 0
if u:
user_id = u['user_id']
if not u:
self.set_status(204)
return
sql += ' AND user_id=%s'
args.append(user_id)
args.append(u['user_id'])

before_id = self.get_argument('before_id', None)
if before_id:
Expand Down Expand Up @@ -62,24 +56,74 @@ async def get(self, channel_id):
else:
self.write_object(list(log))


class User_stats_handler(Api_handler):

async def get(self, channel_id):
await has_mod(self, channel_id)

user = self.get_argument('user')
u = await self.db.fetchone('SELECT user_id FROM twitch_usernames WHERE user=%s', [user])
if not u:
self.set_status(404)
return
user_id = u['user_id']
sql = '''
SELECT
bans, timeouts, purges, chat_messages,
last_viewed_stream_date, streams, streams_row,
streams_row_peak, streams_row_peak_date
streams_row_peak, streams_row_peak_date, sw.watchtime
FROM
twitch_usernames u
LEFT JOIN twitch_user_chat_stats us ON (us.user_id = u.user_id AND us.channel_id=%s)
LEFT JOIN twitch_user_stats s ON (s.user_id = u.user_id AND s.channel_id=%s)
WHERE
u.user = %s
twitch_user_stats s
LEFT JOIN twitch_user_chat_stats us ON (us.user_id = s.user_id AND us.channel_id=s.channel_id)
LEFT JOIN (SELECT user_id, sum(time) as watchtime from twitch_stream_watchtime where channel_id=%s and user_id=%s) sw ON (sw.user_id=s.user_id)
WHERE
s.channel_id = %s AND
s.user_id = %s
'''
stats = await self.db.fetchone(sql, [channel_id, channel_id, user])
stats = await self.db.fetchone(sql, (channel_id, user_id, channel_id, user_id))
stats['watchtime'] = int(stats['watchtime']) if stats['watchtime'] != None else None
if stats:
self.write_object(stats)
else:
self.set_status(204)
self.set_status(204)


class User_streams_watched_handler(Api_handler):

async def get(self, channel_id):
await has_mod(self, channel_id)
user = self.get_argument('user')
args = [user, channel_id]
sql = '''
select
s.started_at, s.uptime, sw.`time` as watchtime,
s.stream_id
from
twitch_usernames u,
twitch_streams s,
twitch_stream_watchtime sw
where
u.user=%s AND
sw.channel_id=%s AND
sw.user_id=u.user_id AND
s.stream_id = sw.stream_id
'''
after_id = self.get_argument('after_id', None)
if after_id:
sql += ' AND started_at < %s'
args.append(parse(after_id))
sql += ' ORDER BY s.started_at DESC LIMIT 5'
streams = await self.db.fetchall(sql, args)
self.write_object(list(streams))


async def has_mod(handler, channel_id):
# TODO: Add setting to allow or disallow non mods to view the logs
# For the moment only mods are allowed to view them
if not handler.current_user:
raise web.HTTPError(401, 'Authentication required')
mod = await handler.db.fetchone('SELECT user_id FROM twitch_channel_mods WHERE channel_id=%s AND user_id=%s',
[channel_id, handler.current_user['user_id']])
if not mod:
raise web.HTTPError(403, 'You are not a moderator of this channel')
134 changes: 56 additions & 78 deletions tbot/web/ui/twitch/logviewer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React from 'react'
import {Link} from 'react-router-dom'
import api from 'tbot/twitch/api'
import qs from 'query-string'
import moment from 'moment'
import {setTitle} from 'tbot/utils'
import {setTitle, iso8601toLocalTime} from 'tbot/utils'
import Loading from 'tbot/components/loading'
import UserInput from './userinput'
import UserStats from './user_stats'
import UserStreamsWatched from './user_streams_watched'
import './logviewer.scss'
import '../dashboard/components/topbar.scss'

Expand All @@ -16,7 +17,6 @@ class Logviewer extends React.Component {
this.query = qs.parse(location.search)
this.state = {
channel: null,
loading: true,
chatlog: [],
loading: true,
loadingChannel: true,
Expand All @@ -40,7 +40,6 @@ class Logviewer extends React.Component {
this.loadChatlog({
before_id: this.query.before_id,
})
this.loadUserChatStats()
})
})
}
Expand All @@ -51,8 +50,8 @@ class Logviewer extends React.Component {
params['message'] = this.query.message
params['show_mod_actions_only'] = this.query.show_mod_actions_only
api.get(`/api/twitch/channels/${this.state.channel.id}/chatlog`, {params: params}).then(r => {
let l = this.state.chatlog;
if ('after_id' in params)
const l = [...this.state.chatlog];
if ('after_id' in params)
l.push(...r.data)
else
l.unshift(...r.data);
Expand All @@ -65,7 +64,6 @@ class Logviewer extends React.Component {
}
}
this.setState({
loading: false,
chatlog: l,
})
}).catch(e => {
Expand All @@ -74,6 +72,8 @@ class Logviewer extends React.Component {
accessDenied: true,
})
}
}).finally(() => {
this.setState({loading: false})
})
}

Expand All @@ -91,63 +91,49 @@ class Logviewer extends React.Component {
})
}

loadUserChatStats() {
this.setState({
userChatStats: null,
loading: true,
})
if (!this.query.user)
return
api.get(`/api/twitch/channels/${this.state.channel.id}/user-chatstats`, {params: {
user: this.query.user,
}}).then(r => {
this.setState({
userChatStats: r.data,
loading: false,
})
})
}

renderChatlog() {
if (this.state.chatlog.length == 0)
if (this.state.chatlog.length == 0) {
if (this.state.loading)
return <Loading text="LOADING" />
else
return <div className="m-2"><center>No results found</center></div>
return <table className="chatlog table table-dark table-striped table-sm table-hover">
<tbody>
{this.state.showLoadBefore?
<tr><td colSpan="3" style={{textAlign: 'center'}}>
{this.state.loading?
<div class="spinner-grow text-primary" role="status"></div>:
<a href="#" onClick={this.loadBefore}>Load more</a>}
</td></tr>
: null}
{this.state.chatlog.map(l => (
<tr key={l.id}>
<td
width="10px"
dateTime={l.created_at}
style={{whiteSpace:'nowrap'}}
>
<a href={`?before_id=${l.id+1}`}>{this.iso8601toLocalTime(l.created_at)}</a>
</td>
<td width="10px"><a href={`?user=${l.user}`}>{l.user}</a></td>
<td>
{this.renderTypeSymbol(l)}
{l.message}
</td>
</tr>
))}
{this.state.showLoadAfter?
<tr><td colSpan="3" style={{textAlign: 'center'}}>
{this.state.loading?
<div class="spinner-grow text-primary" role="status"></div>:
<a href="#" onClick={this.loadAfter}>Load more</a>}
</td></tr>
: null}
</tbody>
</table>
return <div className="m-2"><center>No logs found</center></div>
}
return <div className="chatlog">
<h3>Chat logs</h3>
<table className="table table-dark table-striped table-sm table-hover">
<tbody>
{this.state.showLoadBefore?
<tr><td colSpan="3" style={{textAlign: 'center'}}>
{this.state.loading?
<div className="spinner-grow text-primary" role="status"></div>:
<a href="#" onClick={this.loadBefore}>Load more chat logs</a>}
</td></tr>
: null}
{this.state.chatlog.map(l => (
<tr key={l.id}>
<td
width="10px"
dateTime={l.created_at}
style={{whiteSpace:'nowrap'}}
>
<a href={`?before_id=${l.id+1}`}>{iso8601toLocalTime(l.created_at)}</a>
</td>
<td width="10px"><a href={`?user=${l.user}`}>{l.user}</a></td>
<td>
{this.renderTypeSymbol(l)}
{l.message}
</td>
</tr>
))}
{this.state.showLoadAfter?
<tr><td colSpan="3" style={{textAlign: 'center'}}>
{this.state.loading?
<div className="spinner-grow text-primary" role="status"></div>:
<a href="#" onClick={this.loadAfter}>Load more chat logs</a>}
</td></tr>
: null}
</tbody>
</table>
</div>
}

renderTypeSymbol(l) {
Expand All @@ -170,24 +156,12 @@ class Logviewer extends React.Component {
}
}

iso8601toLocalTime(t) {
let date = moment(t);
return date.format('YYYY-MM-DD HH:mm:ss')
}

renderUserStats() {
if (this.state.userChatStats == null)
return null

return <div className="userChatStats">
<span><b>Total messages:</b> {this.state.userChatStats.chat_messages||0}</span>
<span><b>Timeouts:</b> {this.state.userChatStats.timeouts||0}</span>
<span><b>Bans:</b> {this.state.userChatStats.bans||0}</span>
<span title="Number of watched streams"><b>Streams:</b> {this.state.userChatStats.streams||0}</span>
<span title="Peak streams in a row"><b>Streams peak:</b> {this.state.userChatStats.streams_row_peak||0} ({this.state.userChatStats.streams_row_peak_date||'No data'})</span>
<span title="Date of the latest stream watched"><b>Last stream:</b> {this.state.userChatStats.last_viewed_stream_date||'No data'}</span>
</div>


viewMoreClick = (e) => {
e.preventDefault()

}

renderAccessDenied() {
Expand Down Expand Up @@ -236,9 +210,13 @@ class Logviewer extends React.Component {
<label className="form-check-label" htmlFor="show_mod_actions_only">Show only mod actions</label>
</form>
</div>
{this.renderUserStats()}
{this.query.user?<UserStats channelId={this.state.channel.id} user={this.query.user} />: null}
</div>

{this.renderChatlog()}

{this.query.user?<UserStreamsWatched channelId={this.state.channel.id} user={this.query.user} />: null}

</div>;
}

Expand Down
Loading

0 comments on commit 60ae29a

Please sign in to comment.